第一部分,ES6 中的 Generator
原文地址 http://www.cnblogs.com/wangfupeng1988/p/6532713.html 未經作者允許不得轉載~
在 ES6 出現之前,基本都是各式各樣類似Promise
的解決方案來處理非同步操作的程式碼邏輯,但是 ES6 的Generator
卻給非同步操作又提供了新的思路,馬上就有人給出瞭如何用Generator
來更加優雅的處理非同步操作。
本節內容概述
Generator
簡介Generator
最終如何處理非同步操作- 接下來...
Generator
簡介
先來一段最基礎的Generator
程式碼
function* Hello() { yield 100 yield (function () {return 200})() return 300 } var h = Hello() console.log(typeof h) // object console.log(h.next()) // { value: 100, done: false } console.log(h.next()) // { value: 200, done: false } console.log(h.next()) // { value: 300, done: true } console.log(h.next()) // { value: undefined, done: true }
在 nodejs 環境執行這段程式碼,列印出來的資料都在程式碼註釋中了,也可以自己去試試。將這段程式碼簡單分析一下吧
- 定義
Generator
時,需要使用function*
,其他的和定義函式一樣。內部使用yield
,至於yield
的用處以後再說 - 執行
var h = Hello()
生成一個Generator
物件,經驗驗證typeof h
發現不是普通的函式 - 執行
Hello()
之後,Hello
內部的程式碼不會立即執行,而是出於一個暫停狀態 - 執行第一個
h.next()
時,會啟用剛才的暫停狀態,開始執行Hello
內部的語句,但是,直到遇到yield
語句。一旦遇到yield
語句時,它就會將yield
後面的表示式執行,並返回執行的結果,然後又立即進入暫停狀態。 - 因此第一個
console.log(h.next())
列印出來的是{ value: 100, done: false }
,value
是第一個yield
返回的值,done: false
表示目前處於暫停狀態,尚未執行結束,還可以再繼續往下執行。 - 執行第二個
h.next()
和第一個一樣,不在贅述。此時會執行完第二個yield
後面的表示式並返回結果,然後再次進入暫停狀態 - 執行第三個
h.next()
時,程式會打破暫停狀態,繼續往下執行,但是遇到的不是yield
而是return
。這就預示著,即將執行結束了。因此最後返回的是{ value: 300, done: true }
,done: true
表示執行結束,無法再繼續往下執行了。 - 再去執行第四次
h.next()
時,就只能得到{ value: undefined, done: true }
,因為已經結束,沒有返回值了。
一口氣分析下來,發現並不是那麼簡單,雖然這只是一個最最簡單的Generator
入門程式碼 ———— 可見Generator
的學習成本多高 ———— 但是一旦學會,那將受用無窮!彆著急,跟著我的節奏慢慢來,一行一行程式碼看,你會很快深入瞭解Genarator
但是,你要詳細看一下上面的所有步驟,爭取把我寫的每一步都搞明白。如果搞不明白細節,至少要明白以下幾個要點:
Generator
不是函式,不是函式,不是函式Hello()
不會立即出發執行,而是一上來就暫停- 每次
h.next()
都會打破暫停狀態去執行,直到遇到下一個yield
或者return
- 遇到
yield
時,會執行yeild
後面的表示式,並返回執行之後的值,然後再次進入暫停狀態,此時done: false
。 - 遇到
return
時,會返回值,執行結束,即done: true
- 每次
h.next()
的返回值永遠都是{value: ... , done: ...}
的形式
Generator
最終如何處理非同步操作
上面只是一個最基本最簡單的介紹,但是我們看不到任何與非同步操作相關的事情,那我們接下來就先展示一下最終我們將使用Generator
如何做非同步操作。
之前講解Promise
時候,依次讀取多個檔案,我們是這麼操作的(看不明白的需要回爐重造哈),主要是使用then
做鏈式操作。
readFilePromise('some1.json').then(data => { console.log(data) // 列印第 1 個檔案內容 return readFilePromise('some2.json') }).then(data => { console.log(data) // 列印第 2 個檔案內容 return readFilePromise('some3.json') }).then(data => { console.log(data) // 列印第 3 個檔案內容 return readFilePromise('some4.json') }).then(data=> { console.log(data) // 列印第 4 個檔案內容 })
而如果學會Generator
那麼讀取多個檔案就是如下這樣寫。先不要管如何實現的,光看一看程式碼,你就能比較出哪個更加簡潔、更加易讀、更加所謂的優雅!
co(function* () { const r1 = yield readFilePromise('some1.json') console.log(r1) // 列印第 1 個檔案內容 const r2 = yield readFilePromise('some2.json') console.log(r2) // 列印第 2 個檔案內容 const r3 = yield readFilePromise('some3.json') console.log(r3) // 列印第 3 個檔案內容 const r4 = yield readFilePromise('some4.json') console.log(r4) // 列印第 4 個檔案內容 })
不過,要學到這一步,還需要很長的路要走。不過不要驚慌,也不要請如來佛祖,跟著我的節奏來,認真看,一天包教包會是沒問題的!
接下來...
接下來我們不會立刻講解如何使用Generator
做非同步操作,而是看一看Generator
是一個什麼東西!說來話長,這要從 ES6 的另一個概念Iterator
說起。
第二部分,Iterator 遍歷器
ES6 中引入了很多此前沒有但是卻非常重要的概念,Iterator
就是其中一個。Iterator
物件是一個指標物件,實現類似於單項鍊表的資料結構,通過next()
將指標指向下一個節點 ———— 這裡也就是先簡單做一個概念性的介紹,後面將通過例項為大家演示。
本節演示的程式碼可參考這裡
本節內容概述
- 簡介
Symbol
資料型別 - 具有
[Symbol.iterator]
屬性的資料型別 - 生成
Iterator
物件 Generator
返回的也是Iterator
物件- 接下來...
簡介Symbol
資料型別
Symbol
是一個特殊的資料型別,和number
string
等並列,詳細的教程可參考阮一峰老師 ES6 入門的 Symbol 篇。先看兩句程式
console.log(Array.prototype.slice) // [Function: slice] console.log(Array.prototype[Symbol.iterator]) // [Function: values]
陣列的slice
屬性大家都比較熟悉了,就是一個函式,可以通過Array.prototype.slice
得到。這裡的slice
是一個字串,但是我們獲取Array.prototype[Symbol.iterator]
可以得到一個函式,只不過這裡的[Symbol.iterator]
是Symbol
資料型別,不是字串。但是沒關係,Symbol
資料型別也可以作為物件屬性的key
。如下:
var obj = {} obj.a = 100 obj[Symbol.iterator] = 200 console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}
在此小節中,你只需要知道[Symbol.iterator]
是一個特殊的資料型別Symbol
型別,但是也可以像number
string
型別一樣,作為物件的屬性key
來使用
原生具有[Symbol.iterator]
屬性的資料型別
在 ES6 中,原生具有[Symbol.iterator]
屬性資料型別有:陣列、某些類似陣列的物件(如arguments
、NodeList
)、Set
和Map
。其中,Set
和Map
也是 ES6 中新增的資料型別。
// 陣列 console.log([1, 2, 3][Symbol.iterator]) // function values() { [native code] } // 某些類似陣列的物件,NoeList console.log(document.getElementsByTagName('div')[Symbol.iterator]) // function values() { [native code] }
原生具有[Symbol.iterator]
屬性資料型別有一個特點,就是可以使用for...of
來取值,例如
var item for (item of [100, 200, 300]) { console.log(item) } // 列印出:100 200 300 // 注意,這裡每次獲取的 item 是陣列的 value,而不是 index ,這一點和 傳統 for 迴圈以及 for...in 完全不一樣
而具有[Symbol.iterator]
屬性的物件,都可以一鍵生成一個Iterator
物件。如何生成以及生成之後什麼樣子,還有生成之後的作用,下文分解。
不要著急,也不要跳過本文的任何步驟,一步一步跟著我的節奏來看。
生成Iterator
物件
定義一個陣列,然後生成陣列的Iterator
物件
const arr = [100, 200, 300] const iterator = arr[Symbol.iterator]() // 通過執行 [Symbol.iterator] 的屬性值(函式)來返回一個 iterator 物件
好,現在生成了iterator
,那麼該如何使用它呢 ———— 有兩種方式:next
和for...of
。
先說第一種,next
console.log(iterator.next()) // { value: 100, done: false } console.log(iterator.next()) // { value: 200, done: false } console.log(iterator.next()) // { value: 300, done: false } console.log(iterator.next()) // { value: undefined, done: true }
看到這裡,再結合上一節內容,是不是似曾相識的感覺?(額,沒有的話,那你就回去重新看上一節的內容吧) iterator
物件可以通過next()
方法逐步獲取每個元素的值,以{ value: ..., done: ... }
形式返回,value
就是值,done
表示是否到已經獲取完成。
再說第二種,for...of
let i for (i of iterator) { console.log(i) } // 列印:100 200 300
上面使用for...of
遍歷iterator
物件,可以直接將其值獲取出來。這裡的“值”就對應著上面next()
返回的結果的value
屬性
Generator
返回的也是Iterator
物件
看到這裡,你大體也應該明白了,上一節演示的Generator
,就是生成一個Iterator
物件。因此才會有next()
,也可以通過for...of
來遍歷。拿出上一節的例子再做一次演示:
function* Hello() { yield 100 yield (function () {return 200})() return 300 } const h = Hello() console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]]
執行const h = Hello()
得到的就是一個iterator
物件,因為h[Symbol.iterator]
是有值的。既然是iterator
物件,那麼就可以使用next()
和for...of
進行操作
console.log(h.next()) // { value: 100, done: false } console.log(h.next()) // { value: 200, done: false } console.log(h.next()) // { value: 300, done: false } console.log(h.next()) // { value: undefined, done: true } let i for (i of h) { console.log(i) }
接下來...
這一節我們花費很大力氣,從Iterator
又迴歸到了Generator
,目的就是為了看看Generator
到底是一個什麼東西。瞭解其本質,才能更好的使用它,否則總有一種抓瞎的感覺。
接下來我們就Generator
具體有哪些使用場景。
第三部分,Generator 的具體應用
前面用兩節的內容介紹了Generator
可以讓執行處於暫停狀態,並且知道了Generator
返回的是一個Iterator
物件,這一節就詳細介紹一下Generator
的一些基本用法。
本節演示的程式碼可參考這裡
本節內容概述
next
和yield
引數傳遞for...of
的應用示例yield*
語句Generator
中的this
- 接下來...
next
和yield
引數傳遞
我們之前已經知道,yield
具有返回資料的功能,如下程式碼。yield
後面的資料被返回,存放到返回結果中的value
屬性中。這算是一個方向的引數傳遞。
function* G() { yield 100 } const g = G() console.log( g.next() ) // {value: 100, done: false}
還有另外一個方向的引數傳遞,就是next
向yield
傳遞,如下程式碼。
function* G() { const a = yield 100 console.log('a', a) // a aaa const b = yield 200 console.log('b', b) // b bbb const c = yield 300 console.log('c', c) // c ccc } const g = G() g.next() // value: 100, done: false g.next('aaa') // value: 200, done: false g.next('bbb') // value: 300, done: false g.next('ccc') // value: undefined, done: true
捋一捋上面程式碼的執行過程:
- 執行第一個
g.next()
時,為傳遞任何引數,返回的{value: 100, done: false}
,這個應該沒有疑問 - 執行第二個
g.next('aaa')
時,傳遞的引數是'aaa'
,這個'aaa'
就會被賦值到G
內部的a
標量中,然後執行console.log('a', a)
列印出來,最後返回{value: 200, done: false}
- 執行第三個、第四個時,道理都是完全一樣的,大家自己捋一捋。
有一個要點需要注意,就g.next('aaa')
是將'aaa'
傳遞給上一個已經執行完了的yield
語句前面的變數,而不是即將執行的yield
前面的變數。這句話要能看明白,看不明白就說明剛才的程式碼你還沒看懂,繼續看。
for...of
的應用示例
針對for...of
在Iterator
物件的操作之前已經介紹過了,不過這裡用一個非常好的例子來展示一下。用簡單幾行程式碼實現斐波那契數列。通過之前學過的Generator
知識,應該不能解讀這份程式碼。
function* fibonacci() { let [prev, curr] = [0, 1] for (;;) { [prev, curr] = [curr, prev + curr] // 將中間值通過 yield 返回,並且保留函式執行的狀態,因此可以非常簡單的實現 fibonacci yield curr } } for (let n of fibonacci()) { if (n > 1000) { break } console.log(n) }
yield*
語句
如果有兩個Generator
,想要在第一個中包含第二個,如下需求:
function* G1() { yield 'a' yield 'b' } function* G2() { yield 'x' yield 'y' }
針對以上兩個Generator
,我的需求是:一次輸出a x y b
,該如何做?有同學看到這裡想起了剛剛學到的for..of
可以實現————不錯,確實可以實現(大家也可以想想到底該如何實現)
但是,這要演示一個更加簡潔的方式yield*
表示式
function* G1() { yield 'a' yield* G2() // 使用 yield* 執行 G2() yield 'b' } function* G2() { yield 'x' yield 'y' } for (let item of G1()) { console.log(item) }
之前學過的yield
後面會接一個普通的 JS 物件,而yield*
後面會接一個Generator
,而且會把它其中的yield
按照規則來一步一步執行。如果有多個Generator
串聯使用的話(例如Koa
原始碼中),用yield*
來操作非常方便。
Generator
中的this
對於以下這種寫法,大家可能會和建構函式建立物件的寫法產生混淆,這裡一定要注意 —— Generator 不是函式,更不是建構函式
function* G() {} const g = G()
而以下這種寫法,更加不會成功。只有建構函式才會這麼用,建構函式返回的是this
,而Generator
返回的是一個Iterator
物件。完全是兩碼事,千萬不要搞混了。
function* G() { this.a = 10 } const g = G() console.log(g.a) // 報錯
接下來...
本節基本介紹了Generator
的最常見的用法,但是還是沒有和我們們的最終目的————非同步操作————沾上關係,而且現在看來有點八竿子打不著的關係。但是話說回來,這幾節內容,你也學到了不少知識啊。
別急哈,即便是下一節,它們還不會有聯絡,再下一節就真相大白了。下一節我們又給出一個新概念————Thunk
函式
第四部分,Thunk 函式
要想讓Generator
和非同步操作產生聯絡,就必須過thunk
函式這一關。這一關過了之後,立即就可以著手非同步操作的事情,因此大家再堅持堅持。至於thunk
函式是什麼,下文會詳細演示。
本節演示的程式碼可參考這裡
本節內容概述
- 一個普通的非同步函式
- 封裝成一個
thunk
函式 thunk
函式的特點- 使用
thunkify
庫 - 接下來...
一個普通的非同步函式
就用 nodejs 中讀取檔案的函式為例,通常都這麼寫
fs.readFile('data1.json', 'utf-8', (err, data) => { // 獲取檔案內容 })
其實這個寫法就是將三個引數都傳遞給fs.readFile
這個方法,其中最後一個引數是一個callback
函式。這種函式叫做 多引數函式,我們接下來做一個改造
封裝成一個thunk
函式
改造的程式碼如下所示。不過是不是感覺越改造越複雜了?不過請相信:你看到的複雜僅僅是表面的,這一點東西變的複雜,是為了讓以後更加複雜的東西變得簡單。對於個體而言,隨性比較簡單,遵守規則比較複雜;但是對於整體(包含很多個體)而言,大家都隨性就不好控制了,而大家都遵守規則就很容易管理 ———— 就是這個道理!
const thunk = function (fileName, codeType) { // 返回一個只接受 callback 引數的函式 return function (callback) { fs.readFile(fileName, codeType, callback) } } const readFileThunk = thunk('data1.json', 'utf-8') readFileThunk((err, data) => { // 獲取檔案內容 })
先自己看一看以上程式碼,應該是能看懂的,但是你可能就是看懂了卻不知道這麼做的意義在哪裡。意義先不管,先把它看懂,意義下一節就會看到。
- 執行
const readFileThunk = thunk('data1.json', 'utf-8')
返回的其實是一個函式 readFileThunk
這個函式,只接受一個引數,而且這個引數是一個callback
函式
thunk
函式的特點
就上上面的程式碼,我們經過對傳統的非同步操作函式進行封裝,得到一個只有一個引數的函式,而且這個引數是一個callback
函式,那這就是一個thunk
函式。就像上面程式碼中readFileThunk
一樣。
使用thunkify
庫
上面程式碼的封裝,是我們手動來做的,但是沒遇到一個情況就需要手動做嗎?在這個開源的時代當讓不會這樣,直接使用第三方的thunkify
就好了。
首先要安裝npm i thunkify --save
,然後在程式碼的最上方引用const thunkify = require('thunkify')
。最後,上面我們手動寫的程式碼,完全可以簡化成這幾行,非常簡單!
const thunk = thunkify(fs.readFile) const readFileThunk = thunk('data1.json', 'utf-8') readFileThunk((err, data) => { // 獲取檔案內容 })
接下來...
瞭解了thunk
函式,我們立刻就將Generator
和非同步操作進行結合
第五部分,Generator 與非同步操作
這一節正式開始講解Generator
如何進行非同步操作,以前我們花了好幾節的時間各種打基礎,現在估計大家也都等急了,好戲馬上開始!
本節演示的程式碼可參考這裡
本節內容概述
- 在
Genertor
中使用thunk
函式 - 挨個讀取兩個檔案的內容
- 自驅動流程
- 使用
co
庫 co
庫和Promise
- 接下來...
在Genertor
中使用thunk
函式
這個比較簡單了,之前都講過的,直接看程式碼即可。程式碼中表達的意思,是要依次讀取兩個檔案的內容
const readFileThunk = thunkify(fs.readFile) const gen = function* () { const r1 = yield readFileThunk('data1.json') console.log(r1) const r2 = yield readFileThunk('data2.json') console.log(r2) }
挨個讀取兩個檔案的內容
接著以上的程式碼繼續寫,註釋寫的非常詳細,大家自己去看,看完自己寫程式碼親身體驗。
const g = gen() // 試著列印 g.next() 這裡一定要明白 value 是一個 thunk函式 ,否則下面的程式碼你都看不懂 // console.log( g.next() ) // g.next() 返回 {{ value: thunk函式, done: false }} // 下一行中,g.next().value 是一個 thunk 函式,它需要一個 callback 函式作為引數傳遞進去 g.next().value((err, data1) => { // 這裡的 data1 獲取的就是第一個檔案的內容。下一行中,g.next(data1) 可以將資料傳遞給上面的 r1 變數,此前已經講過這種引數傳遞的形式 // 下一行中,g.next(data1).value 又是一個 thunk 函式,它又需要一個 callback 函式作為引數傳遞進去 g.next(data1).value((err, data2) => { // 這裡的 data2 獲取的是第二個檔案的內容,通過 g.next(data2) 將資料傳遞個上面的 r2 變數 g.next(data2) }) })
上面 6 行左右的程式碼,卻用了 6 行左右的註釋來解釋,可見程式碼的邏輯並不簡單,不過你還是要去盡力理解,否則接下來的內容無法繼續。再說,我已經寫的那麼詳細了,你只要照著仔細看肯定能看明白的。
也許上面的程式碼給你帶來的感覺並不好,第一它邏輯複雜,第二它也不是那麼易讀、簡潔呀,用Generator
實現非同步操作就是這個樣子的?———— 當然不是,繼續往下看。
自驅動流程
以上程式碼中,讀取兩個檔案的內容都是手動一行一行寫的,而我們接下來要做一個自驅動的流程,定義好Generator
的程式碼之後,就讓它自動執行。完整的程式碼如下所示:
// 自動流程管理的函式 function run(generator) { const g = generator() function next(err, data) { const result = g.next(data) // 返回 { value: thunk函式, done: ... } if (result.done) { // result.done 表示是否結束,如果結束了那就 return 作罷 return } result.value(next) // result.value 是一個 thunk 函式,需要一個 callback 函式作為引數,而 next 就是一個 callback 形式的函式 } next() // 手動執行以啟動第一次 next } // 定義 Generator const readFileThunk = thunkify(fs.readFile) const gen = function* () { const r1 = yield readFileThunk('data1.json') console.log(r1.toString()) const r2 = yield readFileThunk('data2.json') console.log(r2.toString()) } // 啟動執行 run(gen)
其實這段程式碼和上面的手動編寫讀取兩個檔案內容的程式碼,原理上是一模一樣的,只不過這裡把流程驅動給封裝起來了。我們簡單分析一下這段程式碼
- 最後一行
run(gen)
之後,進入run
函式內部執行 - 先
const g = generator()
建立Generator
例項,然後定義一個next
方法,並且立即執行next()
- 注意這個
next
函式的引數是err, data
兩個,和我們fs.readFile
用到的callback
函式形式完全一樣 - 第一次執行
next
時,會執行const result = g.next(data)
,而g.next(data)
返回的是{ value: thunk函式, done: ... }
,value
是一個thunk
函式,done
表示是否結束 - 如果
done: true
,那就直接return
了,否則繼續進行 result.value
是一個thunk
函式,需要接受一個callback
函式作為引數傳遞進去,因此正好把next
給傳遞進去,讓next
一直被執行下去
大家照著這個過程來捋一捋,不是特別麻煩,然後自己試著寫完執行一下,基本就能瞭解了。
使用co
庫
剛才我們定義了一個run
還是來做自助流程管理,是不是每次使用都得寫一遍run
函式呢?———— 肯定不是的,直接用大名鼎鼎的co
就好了。用Generator
的工程師,肯定需要用到co
,兩者天生一對,難捨難分。
使用之前請安裝npm i co --save
,然後在檔案開頭引用const co = require('co')
。co
到底有多好用,我們將剛才的程式碼用co
重寫,就變成了如下程式碼。非常簡潔
// 定義 Generator const readFileThunk = thunkify(fs.readFile) const gen = function* () { const r1 = yield readFileThunk('data1.json') console.log(r1.toString()) const r2 = yield readFileThunk('data2.json') console.log(r2.toString()) } const c = co(gen)
而且const c = co(gen)
返回的是一個Promise
物件,可以接著這麼寫
c.then(data => { console.log('結束') })
co
庫和Promise
剛才提到co()
最終返回的是Promise
物件,後知後覺,我們已經忘記Promise
好久了,現在要重新把它拾起來。如果使用co
來處理Generator
的話,其實yield
後面可以跟thunk
函式,也可以跟Promise
物件。
thunk
函式上文一直在演示,下面演示一下Promise
物件的,也權當再回顧一下久別的Promise
。其實從形式上和結果上,都跟thunk
函式一樣。
const readFilePromise = Q.denodeify(fs.readFile) const gen = function* () { const r1 = yield readFilePromise('data1.json') console.log(r1.toString()) const r2 = yield readFilePromise('data2.json') console.log(r2.toString()) } co(gen)
接下來...
經過了前幾節的技術積累,我們用一節的時間就講述了Generator
如何進行非同步操作。接下來要介紹一個開源社群中比較典型的使用Generator
的框架 ———— Koa
第六部分,koa 中使用 Generator
koa 是一個 nodejs 開發的 web 框架,所謂 web 框架就是處理 http 請求的。開源的 nodejs 開發的 web 框架最初是 express。
我們此前說過,既然是處理 http 請求,是一種網路操作,肯定就會用到非同步操作。express 使用的非同步操作是傳統的callbck
,而 koa 用的是我們剛剛講的Generator
(koa v1.x
用的是Generator
,已經被廣泛使用,而 koa v2.x
用到了 ES7 中的async-await
,不過因為 ES7 沒有正式釋出,所以 koa v2.x
也沒有正式釋出,不過可以試用)
koa 是由 express 的原班開發人員開發的,比 express 更加簡潔易用,因此 koa 是目前最為推薦的 nodejs web 框架。阿里前不久就依賴於 koa 開發了自己的 nodejs web 框架 egg
國內可以通過koa.bootcss.com查閱文件,不過這網站依賴了 Google 的服務,因此如果不科學上網,估計會訪問會很慢。
提醒:如果你是初學Generator
而且從來沒有用過 koa ,那麼這一節你如果看不懂,沒有問題。看不懂就不要強求,可以忽略,繼續往下看!
本節演示的程式碼可參考這裡
本節內容概述
- koa 中如何應用
Generator
- koa 的這種應用機制是如何實現的
- 接下來...
koa 中如何應用Generator
koa 是一個 web 框架,處理 http 請求,但是這裡我們不去管它如何處理 http 請求,而是直接關注它使用Genertor
的部分————中介軟體。
例如,我們現在要用 3 個Generator
輸出12345
,我們如下程式碼這麼寫。應該能看明白吧?看不明白回爐重造!
let info = '' function* g1() { info += '1' // 拼接 1 yield* g2() // 拼接 234 info += '5' // 拼接 5 } function* g2() { info += '2' // 拼接 2 yield* g3() // 拼接 3 info += '4' // 拼接 4 } function* g3() { info += '3' // 拼接 3 } var g = g1() g.next() console.log(info) // 12345
但是如果用 koa 的 中介軟體 的思路來做,就需要如下這麼寫。
app.use(function *(next){ this.body = '1'; yield next; this.body += '5'; console.log(this.body); }); app.use(function *(next){ this.body += '2'; yield next; this.body += '4'; }); app.use(function *(next){ this.body += '3'; });
解釋幾個關鍵點
app.use()
中傳入的每一個Generator
就是一個 中介軟體,中介軟體按照傳入的順序排列,順序不能亂- 每個中介軟體內部,
next
表示下一個中介軟體。yield next
就是先將程式暫停,先去執行下一個中介軟體,等next
被執行完之後,再回過頭來執行當前程式碼的下一行。因此,koa 的中介軟體執行順序是一種洋蔥圈模型,不過這裡看不懂也沒問題。 - 每個中介軟體內部,
this
可以共享變數。即第一個中介軟體改變了this
的屬性,在第二個中介軟體中可以看到效果。
koa 的這種應用機制是如何實現的
前方高能————上面介紹 koa 的中間價估計有些新人就開始蒙圈了,不過接下來還有更加有挑戰難度的,就是以上這種方式是如何實現的。你就儘量去看,看懂了更好,看不懂也沒關係————當然,你完全可以選擇跳過本教程直接去看下一篇,這都 OK
加入我們自己實現一個簡單的 koa ———— MyKoa ,那麼僅需要幾十行程式碼就可以搞定上面的問題。直接寫程式碼,注意看重點部分的註釋
class MyKoa extends Object { constructor(props) { super(props); // 儲存所有的中介軟體 this.middlewares = [] } // 注入中介軟體 use (generator) { this.middlewares.push(generator) } // 執行中介軟體 listen () { this._run() } _run () { const ctx = this const middlewares = ctx.middlewares co(function* () { let prev = null let i = middlewares.length //從最後一箇中介軟體到第一個中介軟體的順序開始遍歷 while (i--) { // ctx 作為函式執行時的 this 才能保證多箇中介軟體中資料的共享 //prev 將前面一箇中介軟體傳遞給當前中介軟體,才使得中介軟體裡面的 next 指向下一個中介軟體 prev = middlewares[i].call(ctx, prev); } //執行第一個中介軟體 yield prev; }) } }
最後我們執行程式碼實驗一下效果
var app = new MyKoa(); app.use(function *(next){ this.body = '1'; yield next; this.body += '5'; console.log(this.body); // 12345 }); app.use(function *(next){ this.body += '2'; yield next; this.body += '4'; }); app.use(function *(next){ this.body += '3'; }); app.listen();
接下來...
Generator
的應用基本講完,從一開始的基礎到後面應用到非同步操作,再到本節的高階應用 koa ,算是比較全面了。接下來,我們要再回到最初的起點,探討Generator
的本質,以及它和callback
的關係。
還是那句話,搞明白原理,才能用的更加出色!
第七部分,Generator 的本質是什麼?是否取代了 callback
其實標題中的問題,是一個偽命題,因為Generator
和callback
根本沒有任何關係,只是我們通過一些方式(而且是很複雜的方式)強行將他倆產生了關係,才會有現在的Generator
處理非同步。
本節內容概述
Generator
的本質- 和
callback
的結合
Generator
的本質
介紹Generator
的第一節中,多次提到 暫停 這個詞 ———— “暫停”才是Generator
的本質 ———— 只有Generator
能讓一段程式執行到指定的位置先暫停,然後再啟動,再暫停,再啟動。
而這個 暫停 就很容易讓它和非同步操作產生聯絡,因為我們在處理非同步操作時,即需要一種“開始讀取檔案,然後暫停一下,等著檔案讀取完了,再幹嘛幹嘛...”這樣的需求。因此將Generator
和非同步操作聯絡在一起,並且產生一些比較簡明的解決方案,這是順其自然的事兒,大家要想明白這個道理。
不過,JS 還是 JS,單執行緒還是單執行緒,非同步還是非同步,callback
還是callback
。這一切都不會因為有一個Generator
而有任何變化。
和callback
的結合
之前在介紹Promise
的最後,拿Promise
和callback
做過一些比較,最後發現Promise
其實是利用了callback
才能實現的。而這裡,Generator
也必須利用callback
才能實現。
拿介紹co
時的程式碼舉例(程式碼如下),如果yield
後面用的是thunk
函式,那麼thunk
函式需要的就是一個callback
引數。如果yield
後面用的是Promise
物件,Promise
和callback
的聯絡之前已經介紹過了。
co(function* () { const r1 = yield readFilePromise('some1.json') console.log(r1) // 列印第 1 個檔案內容 const r2 = yield readFileThunk('some2.json') console.log(r2) // 列印第 2 個檔案內容 })
因此,Generator
離不開callback
,Promise
離不開callback
,非同步也離不開callback
。
求打賞
如果你看完了,感覺還不錯,歡迎給我打賞 ———— 以激勵我更多輸出優質內容
最後,github地址是 https://github.com/wangfupeng1988/js-async-tutorial 歡迎 star 和 pr
-------------
學習作者教程:《前端JS高階面試》《前端JS基礎面試題》《React.js模擬大眾點評webapp》《zepto設計與原始碼分析》《json2.js原始碼解讀》