javascript -- 深度解析異步解決方案
高級語言層出不窮, 然而唯 js 鶴立雞群, 這要說道js的設計理念, js天生為異步而生, 正如布道者樸靈在 node深入淺出--(有興趣的可以讀一下, 很有意思^_^) , 異步很早就存在于操作系統的底層, 意外的是,在絕大多數高級編程語言中,異步并不多見,疑似被屏蔽了一搬. 造成這個現象的原因或許令人驚訝, 程序員不太適合通過異步來實現進行程序設計 ^_^.異步的理念是很好的, 然而在程序員編程過程中確實會出現一些問題, 并不是這種理念不容以讓人接受, 而是當有大量的異步操作時會讓你的代碼可讀性降低, 其中回調函數異步編程容易產生毀掉陷阱, 即 callback hell--(不要急, 后面會詳細講解)
然而 js 社區從為停止其腳步, 最新的 ES7 所推出的 async/await 終極異步解決方案, 說終極可能有所不嚴禁, 然而它確實已經完全將原來通過模塊侵入式的異步編程解脫出來, 可以讓程序員以接近傳統意義上的函數調用實現異步編程, 這是 js 里程碑式變革中極其重要的一部分.
Javascript異步編程解決方案歷史與方法
ES 6以前:
-
回調函數
回調函數是最原始的異步編程方案, 上篇文章已經講述, 這里不再累贅, 這里給出傳送門 回調函數之美 然而如果業務邏輯過多時, 回調函數會產生深層嵌套, 對程序員極不友好,
如下代碼所示有一個業務邏輯, 需要對a, b, c三個文件一次讀取var fs = require('fs');fs.readFile('./a.txt', function(err1, data1) {fs.readFile('./b.txt', function(err2, data2) {fs.writeFile('./ab.txt', data1 + data2, function(err) {console.log('read and write done!');});});});
三個異步函數嵌套看起來挺簡單的, 這里知識簡單假設, 拋磚引玉, 如果有5個,10個甚至更多的異步函數要順序執行,那要嵌套(大家都不喜歡身材橫著長吧哈哈)說實話相當恐怖,代碼會變得異常難讀,難調試,難維護。這就是所謂的回調地獄或者callback hell。正是為了解決這個問題,才有了后面兩節要講的內容,用promise或generator進行異步流程管理。異步流程管理說白了就是為了解決回調地獄的問題。所以說任何事情都有兩面性,異步編程有它獨特的優勢,卻也同時遇到了同步編程根本不會有的代碼組織難題。
-
事件監聽(事件發布/訂閱)
事件監聽模式是一種廣泛應用于異步編程的模式, 是回調函數的事件化,即發布/訂閱模式,var util = require('util');var events = require('events');function Stream() {events.EventEmitter.call(this);}util.inherits(Stream, events.EventEmitter)let got = new Stream();got.on("done", function (params) {console.log(params);});got.on("done", function (params) {console.log('QWER');});got.emit("done", 'diyige');console.log('-----------------');var emitter = new events.EventEmitter();emitter.on("done", function (params) {console.log(params);});emitter.on("done", function (params) {console.log('ZXCV');});emitter.emit("done", 'dierge');// diyige// QWER// dierge// ZXCV
-
Promise對象
Promise 是異步編程的一種解決方案,它是比傳統的解決方案——回調函數和事件——更合理和更強大, 它的目的是替換以前回調函數的比不編程方案, 也是后續介紹的異步解決方案的基礎, 它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象, 現在的 js庫幾乎都支持這種異步方案promise對象有以下特點
- 對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變
- 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
下面為單個promise對象應用方法
var promise = new Promise(function(resolve,reject){// ... some codeif(/* 異步操作成功 */){resolve(value);}else{reject(error);}});
通常用promise 的時候我們一般把它相應的業務包裝起來下圖所示模擬了一個讀取文件的異步
promise 函數,
var readFile = function (params) {return new Promise(function(resolve, reject){setTimeout(function(){resolve(params);}, 2000);});}readFile('file1').then(function (data) {console.log(data);return readFile('file2')}).then(function (data) {console.log(data);return readFile('file3')}).then(function (data) {console.log(data);return readFile('file4')}).then(function (data) {console.log(data);return readFile('file5')}).then(function (data) {console.log(data);})//file1//file2//file3//file4//file5
-
流程控制庫
還有一種需要手工調用采能夠處理后續任務的, 在這里只簡單介紹一種, 我們稱之為尾觸發, 常用的關鍵字為 next , 為什么要講到它是因為它是 node 神級框架 express中采用的模式, 這里可能要涉及一些后端node的內容
在 node 搭建服務器時需要面向 切面編程 ,這就需要各種各樣的中間件var app = connect();// Middlewareapp.use(connect.staticCache());app.use(connect.static(__dirname + '/public'));app.use(connect.cookieParser());app.use(connect.session());app.use(connect.query());app.use(connect.bodyParser());app.use(connect.csrf());app.listen(3001);
在通過 use() 方法監聽好一系列中間件后, 監聽端口上的請求, 中間件采用的是尾觸發的機制, 下面是個一個簡單的中間件
function (req, res, next) {// express中間件}
每個中間件傳遞請求對象, 響應對象, 和尾觸發函數, 通過隊列形成一個處理流, 如下圖
中間件機制使得在處理網絡請求時, 可以像面向切面編程一樣進行過濾, 驗證, 日志等功能.
ES 6:
-
Generator函數(協程coroutine)
Generator 函數有多種理解角度。語法上,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。執行函數后返回的是一個遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。function* helloWorldGenerator() {yield 'hello';yield 'world';return 'ending';}var hw = helloWorldGenerator();hw.next()// { value: 'hello', done: false }hw.next()// { value: 'world', done: false }hw.next()// { value: 'ending', done: true }hw.next()// { value: undefined, done: true }
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法可以恢復執行。
-
基于 Promise 對象的自動執行
generater/yield函數還無法真正解決異步方案的問題, 需要配合額外的執行模塊 如 TJ Holowaychuk 的 co 模塊, 在這里用promise模塊進行generater函數的自動執行;var fs = require('fs');var readFile = function (fileName){return new Promise(function (resolve, reject){fs.readFile(fileName, function(error, data){if (error) return reject(error);resolve(data);});});};var gen = function* (){var f1 = yield readFile('/etc/fstab');var f2 = yield readFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());}; /*****************************************var g = gen();g.next().value.then(function(data){g.next(data).value.then(function(data){g.next(data);});}); *****************************************/// 自動執行函數 function run(gen){var g = gen();function next(data){var result = g.next(data);if (result.done) return result.value;result.value.then(function(data){next(data);});}next();}run(gen);
ES 7:
-
async/await
終于來到了我們夢寐以求的的"終極"異步解決方案, 或許你有些失望, 當然這種失望是async/await 僅僅是語法糖, async/await 就是 generater/yield/promise + 自動執行模塊的封裝.相對于前輩 async 函數可以自動執行 并且 await 關鍵字后面則只能帶promise隊形--這里注意 await 后面支持其他數據類型, 但是底層也會將其轉化為promise對象async函數對 Generator 函數的改進,體現在以下四點。
- 內置執行器。
Generator 函數的執行必須靠執行器,所以才有了co模塊,而async函數自帶執行器,這完全不像 Generator 函數,需要調用next方法,或者用co模塊,才能真正執行,得到最后結果。 - 更好的語義。
async和await,比起星號和yield,語義更清楚了。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。 - 更廣的適用性。
co模塊約定,yield命令后面只能是 Thunk 函數或 Promise 對象,而async函數的await命令后面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同于同步操作) -
返回值是 Promise。
async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作。進一步說,async函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。function name(params) {return new Promise(function (resolve, reject) {setTimeout(() => {resolve(params)}, 3000);});}async function myf () {let gf = await name('xiaohua');let gf2 = await name('xiaohong');return gf + gf2 }async function myf3 (params) {let aaa = await myf();return aaa;}myf3().then(function (params) {console.log(params);});// xiaohuaxiaohong
- 內置執行器。
async/await 對前者的generater/yield 進行了高度的封裝配合那些支持 promise 實現的庫可以完美的像普通函數一樣調用, 并且async函數與其他async函數也可以完美無縫連接, 堪稱終極方案koa2已經支持 async/await 但是最新的 express框架依然沒有支持這種寫法, async/await 是大勢所趨, 或許不久的將來 express也會支持它, 我們拭目以待