本文是《深入掌握 ecmascript 6 異步程式設計》系列文章的第一篇。
generator函數的含義與用法 <a href="http://www.ruanyifeng.com/blog/2015/05/thunk.html" target="_blank">thunk函數的含義與用法</a> <a href="http://www.ruanyifeng.com/blog/2015/05/co.html" target="_blank">co函數庫的含義與用法</a> <a href="http://www.ruanyifeng.com/blog/2015/05/async.html" target="_blank">async函數的含義與用法</a>
異步程式設計對 javascript 語言太重要。javascript 隻有一根線程,如果沒有異步程式設計,根本沒法用,非卡死不可。

回調函數 事件監聽 釋出/訂閱 promise 對象
ecmascript 6 (簡稱 es6 )作為下一代 javascript 語言,将 javascript 異步程式設計帶入了一個全新的階段。這組系列文章的主題,就是介紹更強大、更完善的 es6 異步程式設計方法。
新方法比較抽象,初學時,我常常感到費解,直到很久以後才想通,異步程式設計的文法目标,就是怎樣讓它更像同步程式設計。這組系列文章,将幫助你深入了解 javascript 異步程式設計的本質。所有将要講到的内容,都已經實作了。也就是說,馬上就能用,套用一句廣告語,就是"未來已來"。
所謂"異步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。比如,有一個任務是讀取檔案進行處理,異步的執行過程就是下面這樣。
上圖中,任務的第一段是向作業系統送出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統傳回檔案,再接着執行任務的第二段(處理檔案)。
這種不連續的執行,就叫做異步。相應地,連續的執行,就叫做同步。
上圖就是同步的執行方式。由于是連續執行,不能插入其他任務,是以作業系統從硬碟讀取檔案的這段時間,程式隻能幹等着。
javascript 語言對異步程式設計的實作,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裡面,等到重新執行這個任務的時候,就直接調用這個函數。它的英語名字 callback,直譯過來就是"重新調用"。
讀取檔案進行處理,是這樣寫的。
上面代碼中,readfile 函數的第二個參數,就是回調函數,也就是任務的第二段。等到作業系統傳回了 /etc/passwd 這個檔案以後,回調函數才會執行。
一個有趣的問題是,為什麼 node.js 約定,回調函數的第一個參數,必須是錯誤對象err(如果沒有錯誤,該參數就是 null)?原因是執行分成兩段,在這兩段之間抛出的錯誤,程式無法捕捉,隻能當作參數,傳入第二段。
回調函數本身并沒有問題,它的問題出現在多個回調函數嵌套。假定讀取a檔案之後,再讀取b檔案,代碼如下。
promise就是為了解決這個問題而提出的。它不是新的文法功能,而是一種新的寫法,允許将回調函數的橫向加載,改成縱向加載。采用promise,連續讀取多個檔案,寫法如下。
可以看到,promise 的寫法隻是回調函數的改進,使用then方法以後,異步任務的兩段執行看得更清楚了,除此以外,并無新意。
promise 的最大問題是代碼備援,原來的任務被promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
協程有點像函數,又有點像線程。它的運作流程大緻如下。
第一步,協程a開始執行。 第二步,協程a執行到一半,進入暫停,執行權轉移到協程b。 第三步,(一段時間後)協程b交還執行權。 第四步,協程a恢複執行。
上面流程的協程a,就是異步任務,因為它分成兩段(或多段)執行。
舉例來說,讀取檔案的協程寫法如下。
上面代碼的函數 asyncjob 是一個協程,它的奧妙就在其中的 yield 指令。它表示執行到此處,執行權将交給其他協程。也就是說,yield指令是異步兩個階段的分界線。
協程遇到 yield 指令就暫停,等到執行權傳回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield指令,簡直一模一樣。
generator 函數是協程在 es6 的實作,最大特點就是可以交出函數的執行權(即暫停執行)。
上面代碼就是一個 generator 函數。它不同于普通函數,是可以暫停執行的,是以函數名之前要加星号,以示差別。
整個 generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用 yield 語句注明。generator 函數的執行方法如下。
換言之,next 方法的作用是分階段執行 generator 函數。每次調用 next 方法,會傳回一個對象,表示目前階段的資訊( value 屬性和 done 屬性)。value 屬性是 yield 語句後面表達式的值,表示目前階段的值;done 屬性是一個布爾值,表示 generator 函數是否執行完畢,即是否還有下一個階段。
generator 函數可以暫停執行和恢複執行,這是它能封裝異步任務的根本原因。除此之外,它還有兩個特性,使它可以作為異步程式設計的完整解決方案:函數體内外的資料交換和錯誤處理機制。
next 方法傳回值的 value 屬性,是 generator 函數向外輸出資料;next 方法還可以接受參數,這是向 generator 函數體内輸入資料。
上面代碼中,第一個 next 方法的 value 屬性,傳回表達式 x + 2 的值(3)。第二個 next 方法帶有參數2,這個參數可以傳入 generator 函數,作為上個階段異步任務的傳回結果,被函數體内的變量 y 接收。是以,這一步的 value 屬性,傳回的就是2(變量 y 的值)。
generator 函數内部還可以部署錯誤處理代碼,捕獲函數體外抛出的錯誤。
上面代碼的最後一行,generator 函數體外,使用指針對象的 throw 方法抛出的錯誤,可以被函數體内的 try ... catch 代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實作了時間和空間上的分離,這對于異步程式設計無疑是很重要的。
下面看看如何使用 generator 函數,執行一個真實的異步任務。
上面代碼中,generator 函數封裝了一個異步操作,該操作先讀取一個遠端接口,然後從 json 格式的資料解析資訊。就像前面說過的,這段代碼非常像同步操作,除了加上了 yield 指令。
執行這段代碼的方法如下。
(完)