深入理解 JavaScript 非同步系列(4)—— Generator

王福朋發表於2017-03-13

第一部分,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]屬性資料型別有:陣列、某些類似陣列的物件(如argumentsNodeList)、SetMap。其中,SetMap也是 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,那麼該如何使用它呢 ———— 有兩種方式:nextfor...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的一些基本用法。

本節演示的程式碼可參考這裡

本節內容概述

  • nextyield引數傳遞
  • for...of的應用示例
  • yield*語句
  • Generator中的this
  • 接下來...

nextyield引數傳遞

我們之前已經知道,yield具有返回資料的功能,如下程式碼。yield後面的資料被返回,存放到返回結果中的value屬性中。這算是一個方向的引數傳遞。

function* G() {
    yield 100
}
const g = G()
console.log( g.next() ) // {value: 100, done: false}

還有另外一個方向的引數傳遞,就是nextyield傳遞,如下程式碼。

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...ofIterator物件的操作之前已經介紹過了,不過這裡用一個非常好的例子來展示一下。用簡單幾行程式碼實現斐波那契數列。通過之前學過的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

其實標題中的問題,是一個偽命題,因為Generatorcallback根本沒有任何關係,只是我們通過一些方式(而且是很複雜的方式)強行將他倆產生了關係,才會有現在的Generator處理非同步。

本節內容概述

  • Generator的本質
  • callback的結合

Generator的本質

介紹Generator第一節中,多次提到 暫停 這個詞 ———— “暫停”才是Generator的本質 ———— 只有Generator能讓一段程式執行到指定的位置先暫停,然後再啟動,再暫停,再啟動。

而這個 暫停 就很容易讓它和非同步操作產生聯絡,因為我們在處理非同步操作時,即需要一種“開始讀取檔案,然後暫停一下,等著檔案讀取完了,再幹嘛幹嘛...”這樣的需求。因此將Generator和非同步操作聯絡在一起,並且產生一些比較簡明的解決方案,這是順其自然的事兒,大家要想明白這個道理。

不過,JS 還是 JS,單執行緒還是單執行緒,非同步還是非同步,callback還是callback。這一切都不會因為有一個Generator而有任何變化。

callback的結合

之前在介紹Promise的最後,拿Promisecallback做過一些比較,最後發現Promise其實是利用了callback才能實現的。而這裡,Generator也必須利用callback才能實現。

拿介紹co時的程式碼舉例(程式碼如下),如果yield後面用的是thunk函式,那麼thunk函式需要的就是一個callback引數。如果yield後面用的是Promise物件,Promisecallback的聯絡之前已經介紹過了。

co(function* () {
    const r1 = yield readFilePromise('some1.json')
    console.log(r1)  // 列印第 1 個檔案內容
    const r2 = yield readFileThunk('some2.json')
    console.log(r2)  // 列印第 2 個檔案內容
})

因此,Generator離不開callbackPromise離不開callback,非同步也離不開callback

 

求打賞

如果你看完了,感覺還不錯,歡迎給我打賞 ———— 以激勵我更多輸出優質內容

最後,github地址是 https://github.com/wangfupeng1988/js-async-tutorial 歡迎 star 和 pr

-------------

學習作者教程:《前端JS高階面試》《前端JS基礎面試題》《React.js模擬大眾點評webapp》《zepto設計與原始碼分析》《json2.js原始碼解讀

相關文章