文章出處

前言

 上周5在公司作了關于JS異步編程模型的技術分享,可能是內容太干的緣故吧,最后從大家的表情看出“這條粉腸到底在說啥?”的結果:(下面是PPT的講義,具體的PPT和示例代碼在https://github.com/fsjohnhuang/ppt/tree/master/apm_of_js上,有興趣就上去看看吧!

重申主題

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

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秒后執行某項操作

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

那么瀏覽器將假死60秒。正常情況下我們采用異步調用的方式來實現

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

那到底上述兩種方式有什么不同呢?

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

談到它們那必須聯系到IO來說了
阻塞: 就是JS線程發起阻塞IO后,JS線程什么都不做就等則阻塞IO響應。
非阻塞: 就是JS線程發起非阻塞IO后,JS線程可以做其他事,然后通過輪詢、信號量等方式通知JS線程獲取IO響應結果。
也就是說阻塞和非阻塞描述的是發起IO和獲取IO響應之間的時間里,JS線程是否可以繼續處理其他任務。
而同步和異步則是描述另一個方面。

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

同步: 應用程序需要顯式地將數據從內核空間拷貝到用戶空間中,然后再使用數據。
異步: 將數據從內核空間拷貝到用戶空間的操作由系統自動處理,然后通知應用程序直接使用數據即可。

對于如setTimeout等方法而已,本來就存在用戶空間和內核空間的數據通信問題,因此異步更多是描述非阻塞這一特性。
那么異步調用的特點就是:
1. 非阻塞
2. 操作結果將于不明確的未來返回

從Callback Hell說起

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

假設個步驟都是同步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——步驟間存在復雜的狀態依賴
異步執行:->番茄切塊 |->番茄與雞蛋塊一起炒熟
->雞蛋打成蛋液->蛋液煮成半熟->切成蛋塊|

/* 定義任務 */
//番茄切塊
const a = (v番茄, cb = id) => doAsyncIO(a, '番茄塊', cb)
//將雞蛋打成蛋液
const b = (v雞蛋, cb = id) => doAsyncIO(b, '蛋液', cb)
// 蛋液煮成半熟
const c = (v蛋液, cb = id) => doAsyncIO(c, '半熟的雞蛋', cb)
// 切成蛋塊
const d = (v半熟的雞蛋, cb = id) => doAsyncIO(d, '雞蛋塊', cb)
// 番茄與雞蛋塊一起炒熟
const e = (v番茄塊, v雞蛋塊, cb = id) => doAsyncIO(e, '番茄炒雞蛋', cb)

[a,b,c,d,e].forEach(task =>{
    task.state = {done: false}
    task.done = value => {
        task.state = {value, done: true}
    return value
    }
})


/* 執行任務 */
a('番茄')
b('雞蛋', function(v蛋液){
    c(v蛋液, function(v半熟的雞蛋){
        d(v半熟的雞蛋)
    })
})
poll = () => {
     if (a.state.done && d.state.done);else return setTimeout(poll, 100)

     e(a.state.value, d.state.value)
}
setTimeout(poll, 0)

異步調用所帶來的問題是

  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這貨,不說了繼續填坑去:)

尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/6109701.html ^_^肥仔John


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()