深究JS異步編程模型

前言

 上週5在公司做了關於JS異步編程模型的技術分享,多是內容太乾的緣故吧,最後從你們的表情看出「這條粉腸到底在說啥?」的結果:(下面是PPT的講義,具體的PPT和示例代碼在https://github.com/fsjohnhuang/ppt/tree/master/apm_of_js上,有興趣就上去看看吧!javascript

重申主題

 《異步編程模型》這個名稱確實不太直觀,其實今天我想和你們分享的就是上面的代碼是如何演進成下面的代碼而已。php

a(function(){ b(function(){ c(function(){ d() }) }) })

TO前端

;(async function(){ await a() await b() await c() await d() }())

寫在前面

 咱們知道JavaScript是單線程運行的(撇開Web Worker),而且JavaScript線程執行時瀏覽器GUI渲染線程沒法搶佔CPU時間片,所以假如咱們經過如下代碼實現60秒後執行某項操做java

const deadline = Date.now() + 60000 while(deadline > Date.now()); console.log('doSomething')

那麼瀏覽器將假死60秒。正常狀況下咱們採用異步調用的方式來實現git

const deadline = Date.now() + 60000 ;(function _(){ if (deadline > Date.now()){ setTimeout(_, 100) } else{ console.log('doSomething') } }())

那到底上述兩種方式有什麼不一樣呢?

到這裏我有個疑問,那就是到底什麼才叫作異步呢?既然有異步,那必然有同步,那同步又是什麼呢?談起同步和異步,那必不可少地要提起阻塞和非阻塞,那它們又是什麼意思呢?github

談到它們那必須聯繫到IO來講了
阻塞: 就是JS線程發起阻塞IO後,JS線程什麼都不作就等則阻塞IO響應。
非阻塞: 就是JS線程發起非阻塞IO後,JS線程能夠作其餘事,而後經過輪詢、信號量等方式通知JS線程獲取IO響應結果。
也就是說阻塞和非阻塞描述的是發起IO和獲取IO響應之間的時間裏,JS線程是否能夠繼續處理其餘任務。
而同步和異步則是描述另外一個方面。

首先當咱們發起網絡IO請求時,應用程序會向OS發起系統調用,而後內核會調用驅動程序操做網卡,而後網卡獲得的數據會先存放在內核空間中(應用程序是讀取不了的),而後將數據從內核空間拷貝到用戶空間。抽象一下就是,發起IO請求會涉及到用戶空間和內核空間間的數據通訊。編程

同步: 應用程序須要顯式地將數據從內核空間拷貝到用戶空間中,而後再使用數據。
異步: 將數據從內核空間拷貝到用戶空間的操做由系統自動處理,而後通知應用程序直接使用數據便可。promise

對於如setTimeout等方法而已,原本就存在用戶空間和內核空間的數據通訊問題,所以異步更可能是描述非阻塞這一特性。
那麼異步調用的特色就是:
1. 非阻塞
2. 操做結果將於不明確的將來返回瀏覽器

從Callback Hell提及

舉個栗子——番茄炒蛋
番茄切塊(代號a)
雞蛋打成蛋液(代號b)
蛋液煮成半熟(代號c)
將蛋切成塊(代號d)
番茄與雞蛋塊一塊兒炒熟(代號e)markdown

假設個步驟都是同步IO時

->番茄切塊->雞蛋打成蛋液->蛋液煮成半熟->將蛋切成塊->番茄與雞蛋塊一塊兒炒熟

a()
b()
c()
d()
e()

假設個步驟都是異步IO時

 狀況1——全部步驟均無狀態依賴
->番茄切塊
->雞蛋打成蛋液
->蛋液煮成半熟
->將蛋切成塊
->番茄與雞蛋塊一塊兒炒熟

a()
b()
c()
d()
e()

 狀況2——步驟間存在線性的狀態依賴
->番茄切塊->雞蛋打成蛋液->蛋液煮成半熟->將蛋切成塊->番茄與雞蛋塊一塊兒炒熟

a('番茄', function(v番茄塊){ b('雞蛋', function(v蛋液){ c(v蛋液, function(v半熟的雞蛋){ d(v半熟的雞蛋, function(v雞蛋塊){ e(v番茄塊, v雞蛋塊) }) }) }) })

這就是Callback Hell了

 狀況3——步驟間存在複雜的狀態依賴
異步執行:->番茄切塊 |->番茄與雞蛋塊一塊兒炒熟
->雞蛋打成蛋液->蛋液煮成半熟->切成蛋塊|

異步調用所帶來的問題是

  1. 狀態依賴關係難以表達,更沒法使用if...else,while等流程控制語句。
  2. 沒法提供try...catch異常機制來處理異常

初次嘗試——EventProxy

EventProxy做爲一個事件系統,經過after、tail等事件訂閱方法提供帶約束的事件觸發機制,「約束」對應「前置條件」,所以咱們能夠利用這種帶約束的事件觸發機制來做爲異步執行模式下的流程控制表達方式。

const doAsyncIO = (value, cb) => setTimeout(()=>cb(value), Math.random() * 1000) const ep = new EventProxy() /* 定義任務 */ const a = v番茄 => doAsyncIO('番茄塊', ep.emit.bind(ep,'a')) const b = v雞蛋 => doAsyncIO('蛋液', ep.emit.bind(ep,'b')) const c = v蛋液 => doAsyncIO('半熟的雞蛋', ep.emit.bind(ep,'c')) const d = v半熟的雞蛋 => doAsyncIO('雞蛋塊', ep.emit.bind(ep,'d')) const e = (v番茄塊, v雞蛋塊) => doAsyncIO('番茄炒雞蛋', ep.emit.bind(ep,'e')) /* 定義任務間的狀態依賴 */ ep.once('b',c) ep.once('c',d) ep.all('a', 'd', e) /* 執行任務 */ a() b()

另外經過error事件提供對異常機制的支持

ep.on('error', err => { console.log(err) })

但因爲EventProxy採用事件機制來作流程控制,而事件機制好處是下降模塊的耦合度,但從另外一個角度來講會使整個系統結構鬆散難以看出主幹模塊,所以經過事件機制實現流程控制必然致使代碼結構鬆散和邏輯離散,不過這能夠良好的組織形式來讓代碼結構更緊密一些。

曙光的出現——Promise

這裏的Promise指的是已經被ES6歸入囊中的Promises/A+規範及其實現.
Promise至關於咱們去麥當勞點餐後獲得的小票,在將來某個時間點拿着小票就能夠拿到食物。不一樣的是,只要咱們持有Promise實例,不管索取多少次,都能拿到一樣的結果。而麥當勞顯然只能給你一份食物而已。
代碼表現以下

const p1 = new Promise(function(resolve, reject){ /* 工廠函數 * resolve函數表示當前Promise正常結束, 例子: setTimeout(()=>resolve('bingo'), 1000) * reject函數表示當前Promise發生異常, 例子: setTimeout(()=>reject(Error('OMG!')), 1000) */ }) const p2 = p1.then( function fulfilled(val){ return val + 1 } , function rejected(err){ /*處理p1工廠函數中調用reject傳遞來的值*/ } ) const p3 = p2.then( function fulfilled(val){ return new Promise(function(resolve){setTimeout(()=>resolve(val+1), 10000)}) } , function rejected(err){ /*處理p1或p2調用reject或throw error的值*/ } ) p3.catch(function rejected(err){ /*處理p1或p2或p3調用reject或throw error的值*/ } )

Promises/A+中規定Promise狀態爲pending(默認值)、fulfilled或rejected,其中狀態僅能從pending->fulfilled或pending->rejected,而且可經過then和catch訂閱狀態變化事件。狀態變化事件的回調函數執行結果會影響Promise鏈中下一個Promise實例的狀態。另外在觸發Promise狀態變化時是能夠攜帶附加信息的,而且該附加信息將沿着Promise鏈被一直傳遞下去直到被某個Promise的事件回調函數接收爲止。並且Promise還提供Promise.all和Promise.race兩個幫助方法來實現與或的邏輯關係,提供Promsie.resolve來將thenable對象轉換爲Promise對象。
API:
new Promise(function(resolve, reject){}), 帶工廠函數的構造函數
Promise.prototype.then(fulfilled()=>{}, rejected()=>{}),訂閱Promise實例狀態從pending到fulfilled,和從pending到rejected的變化
Promise.prototype.catch(rejected()=>{}),訂閱Promise實例狀態從pending到rejected的變化
Promise.resolve(val), 生成一個狀態爲fulfilled的Promise實例
Promise.reject(val), 生成一個狀態爲rejected的Promise實例
Promise.all(array), 生成一個Promise實例,當array中全部Promise實例狀態均爲fulfilled時,該Promise實例的狀態將從pending轉換爲fulfilled,若array中某個Promise實例的狀態爲rejected,則該實例的狀態將從pending轉換爲rejected.
Promise.race(array), 生成一個Promise實例,當array中某個Promise實例狀態發生轉換,那麼該Promise實例也隨之轉

const doAsyncIO = value => resolve => setTimeout(()=>resolve(value), Math.random() * 1000) /* 定義任務 */ const a = v番茄 => new Promise(doAsyncIO('番茄塊')) const b = v雞蛋 => new Promise(doAsyncIO('蛋液')) const c = v蛋液 => new Promise(doAsyncIO('半熟的雞蛋')) const d = v半熟的雞蛋 => new Promise(doAsyncIO('雞蛋塊')) const e = ([v番茄塊, v雞蛋塊]) => new Promise(doAsyncIO('番茄炒雞蛋')) /* 執行任務 */ Promise.all([ a('番茄'), b('雞蛋').then(c).then(d) ]).then(e) .catch(err=>{ console.log(err) })

最大特色:獨立的可存儲的異步調用結果
其餘特色:fulfilled和rejected函數異步執行

jQuery做爲前端必備工具,也爲咱們提供相似與Promise的工具,那就是jQuery.Deffered

const deffered = $.getJSON('dummy.js') deffered.then(function(val1){ console.log(val1) return !val1 },function (err){ console.log(err) }).then(function(val2){ console.log(val2) })

但jQuery.Deferred並非完整的Promise/A+的實現。
如:

  1. jQuery1.8以前上述代碼val2的值與val1同樣,jQuery1.8及之後上述代碼val2的值就是!val1了。
  2. fulfilled和rejected函數採用同步執行

遺留問題!

const a = () => Promise.resolve('a') const b = (v1) => Promise.resolve('b') const c = (v2, v1) => console.log(v1) a().then(b).then(c)

真正的光明——Coroutine

 Coroutine中文就是協程,意思就是線程間採用協同合做的方式工做,而不是搶佔式的方式工做。因爲JS是單線程運行的,因此這裏的Coroutine就是一個能夠部分執行後退出,後續可在以前退出的地方繼續往下執行的函數.

function coroutine(){ yield console.log('c u later!') console.log('welcome guys!') }

Generator Function

 其實就是迭代器,跟C#的IEnumrable、IEnumerator和Java的Iterable、Iterator同樣。

function* enumerable(){ yield 1 yield 2 } for (let num of enumerable()){ console.log(num) }

 如今咱們將1,2替換爲代碼

function *enumerable(msg){ console.log(msg) var msg1 = yield msg + ' after ' // 斷點 console.log(msg1) var msg2 = yield msg1 + ' after' // 斷點 console.log(msg2 + ' over') }

編譯器會將上述代碼轉換成

const enumerable = function(msg){ var state = -1 return { next: function(val){ switch(++state){ case 0: console.log(msg + ' after') break case 1: var msg1 = val console.log(msg1 + ' after') break case 2: var msg2 = val console.log(msg2 + ' over') break } } } }

經過調用next函數就能夠從以前退出的地方繼續執行了。(條件控制、循環、迭代、異常捕獲處理等就更復雜了)
其實Generator Function實質上就是定義一個有限狀態機,而後經過Generator Function實例的next,throw和return方法觸發狀態遷移。
next(val), 返回{value: val1, done: true|false}
throw(err),在上次執行的位置拋出異常
return(val),狀態機的狀態遷移至終止態,並返回{value: val, done: true}
如今咱們用Gererator Function來作番茄炒蛋

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000) /* 定義任務 */ const a = v番茄 => new Promise(doAsyncIO('番茄塊')) const b = v雞蛋 => new Promise(doAsyncIO('蛋液')) const c = v蛋液 => new Promise(doAsyncIO('半熟的雞蛋')) const d = v半熟的雞蛋 => new Promise(doAsyncIO('雞蛋塊')) const e = (v番茄塊, v雞蛋塊) => new Promise(doAsyncIO('番茄炒雞蛋')) function* coroutineFunction(){ try{ var p番茄塊 = a('番茄') var v蛋液 = yield b('雞蛋') var v半熟的雞蛋 = yield c(v蛋液) var v雞蛋塊 = yield d(v半熟的雞蛋) var v番茄塊 = yield p番茄塊 var v番茄抄雞蛋 = yield e(v番茄塊, v雞蛋塊) } catch(e){ console.log(e.message) } } const coroutine = coroutineFunction() throwError = coroutine.throw.bind(coroutine) coroutine.next().value.then(function(v蛋液){ coroutine.next(v蛋液).then(function(v半熟的雞蛋){ coroutine.next(v半熟的雞蛋).then(function(v雞蛋塊){ coroutine.next().then(function(v番茄塊){ coroutine.next(v番茄塊).then(function(v番茄抄雞蛋){ coroutine.next(v番茄抄雞蛋) }, throwError) }, throwError) }, throwError) }, throwError) })

 悲催又回到Callback hell.但咱們能夠發現coroutineFunction實際上是以同步代碼的風格來定義任務間的執行順序(狀態依賴)而已,執行模塊在後面這個讓人頭痛的Callback hell那裏,而且這個Callback Hell是根據coroutineFunction的內容生成,像這種重複有意義的事情天然由機器幫咱們處理最爲恰當了,因而咱們引入個狀態管理器獲得

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000) /* 定義任務 */ const a = v番茄 => new Promise(doAsyncIO('番茄塊')) const b = v雞蛋 => new Promise(doAsyncIO('蛋液')) const c = v蛋液 => new Promise(doAsyncIO('半熟的雞蛋')) const d = v半熟的雞蛋 => new Promise(doAsyncIO('雞蛋塊')) const e = (v番茄塊, v雞蛋塊) => new Promise(doAsyncIO('番茄炒雞蛋')) function* coroutineFunction(){ try{ var p番茄塊 = a('番茄') var v蛋液 = yield b('雞蛋') var v半熟的雞蛋 = yield c(v蛋液) var v雞蛋塊 = yield d(v半熟的雞蛋) var v番茄塊 = yield p番茄塊 var v番茄抄雞蛋 = yield e(v番茄塊, v雞蛋塊) } catch(e){ console.log(e.message) } } iPromise(coroutineFunction)

 舒爽多了!

async和await

ES7引入了async和await兩個關鍵字,Node.js7支持這兩貨。因而Coroutine寫法就更酸爽了.

const doAsyncIO = value => (resolve) => setTimeout(()=>resolve(value), Math.random() * 1000) /* 定義任務 */ const a = v番茄 => new Promise(doAsyncIO('番茄塊')) const b = v雞蛋 => new Promise(doAsyncIO('蛋液')) const c = v蛋液 => new Promise(doAsyncIO('半熟的雞蛋')) const d = v半熟的雞蛋 => new Promise(doAsyncIO('雞蛋塊')) const e = (v番茄塊, v雞蛋塊) => new Promise(doAsyncIO('番茄炒雞蛋')) async function coroutine(){ try{ var p番茄塊 = a('番茄') var v蛋液 = await b('雞蛋') var v半熟的雞蛋 = await c(v蛋液) var v雞蛋塊 = await d(v半熟的雞蛋) var v番茄塊 = await p番茄塊 var v番茄抄雞蛋 = await e(v番茄塊, v雞蛋塊) } catch(e){ console.log(e.message) } } coroutine()

總結

到這裏各位應該會想「不就作個西紅柿炒雞蛋嗎,搞這麼多,至於嗎?」。其實個人見解是

  1. 對於狀態依賴簡單的狀況下,callback的方式足矣;
  2. 對於狀態依賴複雜(譬如作個佛跳牆等大菜時),Promise或Coroutine顯然會讓代碼更簡潔直觀,更容易測試所以bug更少,更容易維護所以更易被優化。

我曾夢想有一天全部瀏覽器都支持Promise,async和await,你們能夠不明就裏地寫出coroutine,完美地處理異步調用的各類問題。直到有一天知道世上又多了Rxjs這貨,不說了繼續填坑去:) 

相關文章
相關標籤/搜索