第一部分,ES6 中的 Generator
原文地址 http://www.cnblogs.com/wangfupeng1988/p/6532713.html 未經(jīng)作者允許不得轉(zhuǎn)載~
在 ES6 出現(xiàn)之前,基本都是各式各樣類似Promise
的解決方案來處理異步操作的代碼邏輯,但是 ES6 的Generator
卻給異步操作又提供了新的思路,馬上就有人給出了如何用Generator
來更加優(yōu)雅的處理異步操作。
本節(jié)內(nèi)容概述
Generator
簡(jiǎn)介Generator
最終如何處理異步操作接下來...
Generator
簡(jiǎn)介
先來一段最基礎(chǔ)的Generator
代碼
function* Hello() { yield 100 yield (function () {return 200})() return 300}var h = Hello() console.log(typeof h) // objectconsole.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 環(huán)境執(zhí)行這段代碼,打印出來的數(shù)據(jù)都在代碼注釋中了,也可以自己去試試。將這段代碼簡(jiǎn)單分析一下吧
定義
Generator
時(shí),需要使用function*
,其他的和定義函數(shù)一樣。內(nèi)部使用yield
,至于yield
的用處以后再說執(zhí)行
var h = Hello()
生成一個(gè)Generator
對(duì)象,經(jīng)驗(yàn)驗(yàn)證typeof h
發(fā)現(xiàn)不是普通的函數(shù)執(zhí)行
Hello()
之后,Hello
內(nèi)部的代碼不會(huì)立即執(zhí)行,而是出于一個(gè)暫停狀態(tài)執(zhí)行第一個(gè)
h.next()
時(shí),會(huì)激活剛才的暫停狀態(tài),開始執(zhí)行Hello
內(nèi)部的語(yǔ)句,但是,直到遇到yield
語(yǔ)句。一旦遇到yield
語(yǔ)句時(shí),它就會(huì)將yield
后面的表達(dá)式執(zhí)行,并返回執(zhí)行的結(jié)果,然后又立即進(jìn)入暫停狀態(tài)。因此第一個(gè)
console.log(h.next())
打印出來的是{ value: 100, done: false }
,value
是第一個(gè)yield
返回的值,done: false
表示目前處于暫停狀態(tài),尚未執(zhí)行結(jié)束,還可以再繼續(xù)往下執(zhí)行。執(zhí)行第二個(gè)
h.next()
和第一個(gè)一樣,不在贅述。此時(shí)會(huì)執(zhí)行完第二個(gè)yield
后面的表達(dá)式并返回結(jié)果,然后再次進(jìn)入暫停狀態(tài)執(zhí)行第三個(gè)
h.next()
時(shí),程序會(huì)打破暫停狀態(tài),繼續(xù)往下執(zhí)行,但是遇到的不是yield
而是return
。這就預(yù)示著,即將執(zhí)行結(jié)束了。因此最后返回的是{ value: 300, done: true }
,done: true
表示執(zhí)行結(jié)束,無(wú)法再繼續(xù)往下執(zhí)行了。再去執(zhí)行第四次
h.next()
時(shí),就只能得到{ value: undefined, done: true }
,因?yàn)橐呀?jīng)結(jié)束,沒有返回值了。
一口氣分析下來,發(fā)現(xiàn)并不是那么簡(jiǎn)單,雖然這只是一個(gè)最最簡(jiǎn)單的Generator
入門代碼 ———— 可見Generator
的學(xué)習(xí)成本多高 ———— 但是一旦學(xué)會(huì),那將受用無(wú)窮!別著急,跟著我的節(jié)奏慢慢來,一行一行代碼看,你會(huì)很快深入了解Genarator
但是,你要詳細(xì)看一下上面的所有步驟,爭(zhēng)取把我寫的每一步都搞明白。如果搞不明白細(xì)節(jié),至少要明白以下幾個(gè)要點(diǎn):
Generator
不是函數(shù),不是函數(shù),不是函數(shù)Hello()
不會(huì)立即出發(fā)執(zhí)行,而是一上來就暫停每次
h.next()
都會(huì)打破暫停狀態(tài)去執(zhí)行,直到遇到下一個(gè)yield
或者return
遇到
yield
時(shí),會(huì)執(zhí)行yeild
后面的表達(dá)式,并返回執(zhí)行之后的值,然后再次進(jìn)入暫停狀態(tài),此時(shí)done: false
。遇到
return
時(shí),會(huì)返回值,執(zhí)行結(jié)束,即done: true
每次
h.next()
的返回值永遠(yuǎn)都是{value: ... , done: ...}
的形式
Generator
最終如何處理異步操作
上面只是一個(gè)最基本最簡(jiǎn)單的介紹,但是我們看不到任何與異步操作相關(guān)的事情,那我們接下來就先展示一下最終我們將使用Generator
如何做異步操作。
之前講解Promise
時(shí)候,依次讀取多個(gè)文件,我們是這么操作的(看不明白的需要回爐重造哈),主要是使用then
做鏈?zhǔn)讲僮鳌?/p>
readFilePromise('some1.json').then(data => { console.log(data) // 打印第 1 個(gè)文件內(nèi)容 return readFilePromise('some2.json') }).then(data => { console.log(data) // 打印第 2 個(gè)文件內(nèi)容 return readFilePromise('some3.json') }).then(data => { console.log(data) // 打印第 3 個(gè)文件內(nèi)容 return readFilePromise('some4.json') }).then(data=> { console.log(data) // 打印第 4 個(gè)文件內(nèi)容})
而如果學(xué)會(huì)Generator
那么讀取多個(gè)文件就是如下這樣寫。先不要管如何實(shí)現(xiàn)的,光看一看代碼,你就能比較出哪個(gè)更加簡(jiǎn)潔、更加易讀、更加所謂的優(yōu)雅!
co(function* () { const r1 = yield readFilePromise('some1.json') console.log(r1) // 打印第 1 個(gè)文件內(nèi)容 const r2 = yield readFilePromise('some2.json') console.log(r2) // 打印第 2 個(gè)文件內(nèi)容 const r3 = yield readFilePromise('some3.json') console.log(r3) // 打印第 3 個(gè)文件內(nèi)容 const r4 = yield readFilePromise('some4.json') console.log(r4) // 打印第 4 個(gè)文件內(nèi)容})
不過,要學(xué)到這一步,還需要很長(zhǎng)的路要走。不過不要驚慌,也不要請(qǐng)如來佛祖,跟著我的節(jié)奏來,認(rèn)真看,一天包教包會(huì)是沒問題的!
接下來...
接下來我們不會(huì)立刻講解如何使用Generator
做異步操作,而是看一看Generator
是一個(gè)什么東西!說來話長(zhǎng),這要從 ES6 的另一個(gè)概念Iterator
說起。
第二部分,Iterator 遍歷器
ES6 中引入了很多此前沒有但是卻非常重要的概念,Iterator
就是其中一個(gè)。Iterator
對(duì)象是一個(gè)指針對(duì)象,實(shí)現(xiàn)類似于單項(xiàng)鏈表的數(shù)據(jù)結(jié)構(gòu),通過next()
將指針指向下一個(gè)節(jié)點(diǎn) ———— 這里也就是先簡(jiǎn)單做一個(gè)概念性的介紹,后面將通過實(shí)例為大家演示。
本節(jié)演示的代碼可參考這里
本節(jié)內(nèi)容概述
簡(jiǎn)介
Symbol
數(shù)據(jù)類型具有
[Symbol.iterator]
屬性的數(shù)據(jù)類型生成
Iterator
對(duì)象Generator
返回的也是Iterator
對(duì)象接下來...
簡(jiǎn)介Symbol
數(shù)據(jù)類型
Symbol
是一個(gè)特殊的數(shù)據(jù)類型,和number
string
等并列,詳細(xì)的教程可參考阮一峰老師 ES6 入門的 Symbol 篇。先看兩句程序
console.log(Array.prototype.slice) // [Function: slice]console.log(Array.prototype[Symbol.iterator]) // [Function: values]
數(shù)組的slice
屬性大家都比較熟悉了,就是一個(gè)函數(shù),可以通過Array.prototype.slice
得到。這里的slice
是一個(gè)字符串,但是我們獲取Array.prototype[Symbol.iterator]
可以得到一個(gè)函數(shù),只不過這里的[Symbol.iterator]
是Symbol
數(shù)據(jù)類型,不是字符串。但是沒關(guān)系,Symbol
數(shù)據(jù)類型也可以作為對(duì)象屬性的key
。如下:
var obj = {} obj.a = 100obj[Symbol.iterator] = 200console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}
在此小節(jié)中,你只需要知道[Symbol.iterator]
是一個(gè)特殊的數(shù)據(jù)類型Symbol
類型,但是也可以像number
string
類型一樣,作為對(duì)象的屬性key
來使用
原生具有[Symbol.iterator]
屬性的數(shù)據(jù)類型
在 ES6 中,原生具有[Symbol.iterator]
屬性數(shù)據(jù)類型有:數(shù)組、某些類似數(shù)組的對(duì)象(如arguments
、NodeList
)、Set
和Map
。其中,Set
和Map
也是 ES6 中新增的數(shù)據(jù)類型。
// 數(shù)組console.log([1, 2, 3][Symbol.iterator]) // function values() { [native code] }// 某些類似數(shù)組的對(duì)象,NoeListconsole.log(document.getElementsByTagName('div')[Symbol.iterator]) // function values() { [native code] }
原生具有[Symbol.iterator]
屬性數(shù)據(jù)類型有一個(gè)特點(diǎn),就是可以使用for...of
來取值,例如
var itemfor (item of [100, 200, 300]) { console.log(item) }// 打印出:100 200 300 // 注意,這里每次獲取的 item 是數(shù)組的 value,而不是 index ,這一點(diǎn)和 傳統(tǒng) for 循環(huán)以及 for...in 完全不一樣
而具有[Symbol.iterator]
屬性的對(duì)象,都可以一鍵生成一個(gè)Iterator
對(duì)象。如何生成以及生成之后什么樣子,還有生成之后的作用,下文分解。
不要著急,也不要跳過本文的任何步驟,一步一步跟著我的節(jié)奏來看。
生成Iterator
對(duì)象
定義一個(gè)數(shù)組,然后生成數(shù)組的Iterator
對(duì)象
const arr = [100, 200, 300] const iterator = arr[Symbol.iterator]() // 通過執(zhí)行 [Symbol.iterator] 的屬性值(函數(shù))來返回一個(gè) iterator 對(duì)象
好,現(xiàn)在生成了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 }
看到這里,再結(jié)合上一節(jié)內(nèi)容,是不是似曾相識(shí)的感覺?(額,沒有的話,那你就回去重新看上一節(jié)的內(nèi)容吧) iterator
對(duì)象可以通過next()
方法逐步獲取每個(gè)元素的值,以{ value: ..., done: ... }
形式返回,value
就是值,done
表示是否到已經(jīng)獲取完成。
再說第二種,for...of
let ifor (i of iterator) { console.log(i) }// 打印:100 200 300
上面使用for...of
遍歷iterator
對(duì)象,可以直接將其值獲取出來。這里的“值”就對(duì)應(yīng)著上面next()
返回的結(jié)果的value
屬性
Generator
返回的也是Iterator
對(duì)象
看到這里,你大體也應(yīng)該明白了,上一節(jié)演示的Generator
,就是生成一個(gè)Iterator
對(duì)象。因此才會(huì)有next()
,也可以通過for...of
來遍歷。拿出上一節(jié)的例子再做一次演示:
function* Hello() { yield 100 yield (function () {return 200})() return 300 } const h = Hello() console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]]
執(zhí)行const h = Hello()
得到的就是一個(gè)iterator
對(duì)象,因?yàn)?code style="margin: 0px; padding: 0px;">h[Symbol.iterator]是有值的。既然是iterator
對(duì)象,那么就可以使用next()
和for...of
進(jìn)行操作
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 ifor (i of h) { console.log(i) }
接下來...
這一節(jié)我們花費(fèi)很大力氣,從Iterator
又回歸到了Generator
,目的就是為了看看Generator
到底是一個(gè)什么東西。了解其本質(zhì),才能更好的使用它,否則總有一種抓瞎的感覺。
接下來我們就Generator
具體有哪些使用場(chǎng)景。
第三部分,Generator 的具體應(yīng)用
前面用兩節(jié)的內(nèi)容介紹了Generator
可以讓執(zhí)行處于暫停狀態(tài),并且知道了Generator
返回的是一個(gè)Iterator
對(duì)象,這一節(jié)就詳細(xì)介紹一下Generator
的一些基本用法。
本節(jié)演示的代碼可參考這里
本節(jié)內(nèi)容概述
next
和yield
參數(shù)傳遞for...of
的應(yīng)用示例yield*
語(yǔ)句Generator
中的this
接下來...
next
和yield
參數(shù)傳遞
我們之前已經(jīng)知道,yield
具有返回?cái)?shù)據(jù)的功能,如下代碼。yield
后面的數(shù)據(jù)被返回,存放到返回結(jié)果中的value
屬性中。這算是一個(gè)方向的參數(shù)傳遞。
function* G() { yield 100} const g = G() console.log( g.next() ) // {value: 100, done: false}
還有另外一個(gè)方向的參數(shù)傳遞,就是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: falseg.next('aaa') // value: 200, done: falseg.next('bbb') // value: 300, done: falseg.next('ccc') // value: undefined, done: true
捋一捋上面代碼的執(zhí)行過程:
執(zhí)行第一個(gè)
g.next()
時(shí),為傳遞任何參數(shù),返回的{value: 100, done: false}
,這個(gè)應(yīng)該沒有疑問執(zhí)行第二個(gè)
g.next('aaa')
時(shí),傳遞的參數(shù)是'aaa'
,這個(gè)'aaa'
就會(huì)被賦值到G
內(nèi)部的a
標(biāo)量中,然后執(zhí)行console.log('a', a)
打印出來,最后返回{value: 200, done: false}
執(zhí)行第三個(gè)、第四個(gè)時(shí),道理都是完全一樣的,大家自己捋一捋。
有一個(gè)要點(diǎn)需要注意,就g.next('aaa')
是將'aaa'
傳遞給上一個(gè)已經(jīng)執(zhí)行完了的yield
語(yǔ)句前面的變量,而不是即將執(zhí)行的yield
前面的變量。這句話要能看明白,看不明白就說明剛才的代碼你還沒看懂,繼續(xù)看。
for...of
的應(yīng)用示例
針對(duì)for...of
在Iterator
對(duì)象的操作之前已經(jīng)介紹過了,不過這里用一個(gè)非常好的例子來展示一下。用簡(jiǎn)單幾行代碼實(shí)現(xiàn)斐波那契數(shù)列。通過之前學(xué)過的Generator
知識(shí),應(yīng)該不能解讀這份代碼。
function* fibonacci() { let [prev, curr] = [0, 1] for (;;) { [prev, curr] = [curr, prev + curr] // 將中間值通過 yield 返回,并且保留函數(shù)執(zhí)行的狀態(tài),因此可以非常簡(jiǎn)單的實(shí)現(xiàn) fibonacci yield curr } }for (let n of fibonacci()) { if (n > 1000) { break } console.log(n) }
yield*
語(yǔ)句
如果有兩個(gè)Generator
,想要在第一個(gè)中包含第二個(gè),如下需求:
function* G1() { yield 'a' yield 'b'}function* G2() { yield 'x' yield 'y'}
針對(duì)以上兩個(gè)Generator
,我的需求是:一次輸出a x y b
,該如何做?有同學(xué)看到這里想起了剛剛學(xué)到的for..of
可以實(shí)現(xiàn)————不錯(cuò),確實(shí)可以實(shí)現(xiàn)(大家也可以想想到底該如何實(shí)現(xiàn))
但是,這要演示一個(gè)更加簡(jiǎn)潔的方式yield*
表達(dá)式
function* G1() { yield 'a' yield* G2() // 使用 yield* 執(zhí)行 G2() yield 'b'}function* G2() { yield 'x' yield 'y'}for (let item of G1()) { console.log(item) }
之前學(xué)過的yield
后面會(huì)接一個(gè)普通的 JS 對(duì)象,而yield*
后面會(huì)接一個(gè)Generator
,而且會(huì)把它其中的yield
按照規(guī)則來一步一步執(zhí)行。如果有多個(gè)Generator
串聯(lián)使用的話(例如Koa
源碼中),用yield*
來操作非常方便。
Generator
中的this
對(duì)于以下這種寫法,大家可能會(huì)和構(gòu)造函數(shù)創(chuàng)建對(duì)象的寫法產(chǎn)生混淆,這里一定要注意 —— Generator 不是函數(shù),更不是構(gòu)造函數(shù)
function* G() {} const g = G()
而以下這種寫法,更加不會(huì)成功。只有構(gòu)造函數(shù)才會(huì)這么用,構(gòu)造函數(shù)返回的是this
,而Generator
返回的是一個(gè)Iterator
對(duì)象。完全是兩碼事,千萬(wàn)不要搞混了。
function* G() { this.a = 10} const g = G() console.log(g.a) // 報(bào)錯(cuò)
接下來...
本節(jié)基本介紹了Generator
的最常見的用法,但是還是沒有和咱們的最終目的————異步操作————沾上關(guān)系,而且現(xiàn)在看來有點(diǎn)八竿子打不著的關(guān)系。但是話說回來,這幾節(jié)內(nèi)容,你也學(xué)到了不少知識(shí)啊。
別急哈,即便是下一節(jié),它們還不會(huì)有聯(lián)系,再下一節(jié)就真相大白了。下一節(jié)我們又給出一個(gè)新概念————Thunk
函數(shù)
第四部分,Thunk 函數(shù)
要想讓Generator
和異步操作產(chǎn)生聯(lián)系,就必須過thunk
函數(shù)這一關(guān)。這一關(guān)過了之后,立即就可以著手異步操作的事情,因此大家再堅(jiān)持堅(jiān)持。至于thunk
函數(shù)是什么,下文會(huì)詳細(xì)演示。
本節(jié)演示的代碼可參考這里
本節(jié)內(nèi)容概述
一個(gè)普通的異步函數(shù)
封裝成一個(gè)
thunk
函數(shù)thunk
函數(shù)的特點(diǎn)使用
thunkify
庫(kù)接下來...
一個(gè)普通的異步函數(shù)
就用 nodejs 中讀取文件的函數(shù)為例,通常都這么寫
fs.readFile('data1.json', 'utf-8', (err, data) => { // 獲取文件內(nèi)容})
其實(shí)這個(gè)寫法就是將三個(gè)參數(shù)都傳遞給fs.readFile
這個(gè)方法,其中最后一個(gè)參數(shù)是一個(gè)callback
函數(shù)。這種函數(shù)叫做 多參數(shù)函數(shù),我們接下來做一個(gè)改造
封裝成一個(gè)thunk
函數(shù)
改造的代碼如下所示。不過是不是感覺越改造越復(fù)雜了?不過請(qǐng)相信:你看到的復(fù)雜僅僅是表面的,這一點(diǎn)東西變的復(fù)雜,是為了讓以后更加復(fù)雜的東西變得簡(jiǎn)單。對(duì)于個(gè)體而言,隨性比較簡(jiǎn)單,遵守規(guī)則比較復(fù)雜;但是對(duì)于整體(包含很多個(gè)體)而言,大家都隨性就不好控制了,而大家都遵守規(guī)則就很容易管理 ———— 就是這個(gè)道理!
const thunk = function (fileName, codeType) { // 返回一個(gè)只接受 callback 參數(shù)的函數(shù) return function (callback) { fs.readFile(fileName, codeType, callback) } } const readFileThunk = thunk('data1.json', 'utf-8') readFileThunk((err, data) => { // 獲取文件內(nèi)容})
先自己看一看以上代碼,應(yīng)該是能看懂的,但是你可能就是看懂了卻不知道這么做的意義在哪里。意義先不管,先把它看懂,意義下一節(jié)就會(huì)看到。
執(zhí)行
const readFileThunk = thunk('data1.json', 'utf-8')
返回的其實(shí)是一個(gè)函數(shù)readFileThunk
這個(gè)函數(shù),只接受一個(gè)參數(shù),而且這個(gè)參數(shù)是一個(gè)callback
函數(shù)
thunk
函數(shù)的特點(diǎn)
就上上面的代碼,我們經(jīng)過對(duì)傳統(tǒng)的異步操作函數(shù)進(jìn)行封裝,得到一個(gè)只有一個(gè)參數(shù)的函數(shù),而且這個(gè)參數(shù)是一個(gè)callback
函數(shù),那這就是一個(gè)thunk
函數(shù)。就像上面代碼中readFileThunk
一樣。
使用thunkify
庫(kù)
上面代碼的封裝,是我們手動(dòng)來做的,但是沒遇到一個(gè)情況就需要手動(dòng)做嗎?在這個(gè)開源的時(shí)代當(dāng)讓不會(huì)這樣,直接使用第三方的thunkify
就好了。
首先要安裝npm i thunkify --save
,然后在代碼的最上方引用const thunkify = require('thunkify')
。最后,上面我們手動(dòng)寫的代碼,完全可以簡(jiǎn)化成這幾行,非常簡(jiǎn)單!
const thunk = thunkify(fs.readFile) const readFileThunk = thunk('data1.json', 'utf-8') readFileThunk((err, data) => { // 獲取文件內(nèi)容})
接下來...
了解了thunk
函數(shù),我們立刻就將Generator
和異步操作進(jìn)行結(jié)合
第五部分,Generator 與異步操作
這一節(jié)正式開始講解Generator
如何進(jìn)行異步操作,以前我們花了好幾節(jié)的時(shí)間各種打基礎(chǔ),現(xiàn)在估計(jì)大家也都等急了,好戲馬上開始!
本節(jié)演示的代碼可參考這里
本節(jié)內(nèi)容概述
在
Genertor
中使用thunk
函數(shù)挨個(gè)讀取兩個(gè)文件的內(nèi)容
自驅(qū)動(dòng)流程
使用
co
庫(kù)co
庫(kù)和Promise
接下來...