一.Generator函數(shù)的概念
????Generator函數(shù)是 ES6 提供的一種異步編程解決方案。前面討論過的Promise對(duì)象也是ES6提供的異步解決方案,為什么還要提出Generator呢。
????使用Promise對(duì)象處理異步固然有不少優(yōu)勢(shì),尤其是可以將回調(diào)地獄的處理變?yōu)閠hen的鏈?zhǔn)秸{(diào)用。但也不可避免的存在一些缺點(diǎn),例如經(jīng)過Promise包裝的異步會(huì)包含大量的Promise名詞(resolve,reject,then...),可讀性不好。
????其實(shí),異步任務(wù)的最佳處理方式應(yīng)當(dāng)是像操作同步任務(wù)那樣操作異步任務(wù),即異步任務(wù)之后的代碼直接寫在異步下面,而不是寫在回調(diào)函數(shù)或then方法中。Generator 函數(shù)的提出就是為了解決這個(gè)問題。如何做到將異步的操作同步化呢。試想,我們?nèi)绻苜x予函數(shù)'暫停'執(zhí)行的功能,即遇到異步任務(wù)時(shí),將當(dāng)前上下文的狀態(tài)暫存起來,等到異步任務(wù)結(jié)束后,拿到異步結(jié)果再繼續(xù)向下執(zhí)行,這樣就能實(shí)現(xiàn)上述需求。這就是Generator 函數(shù)的異步處理思想。
????如何能實(shí)現(xiàn)函數(shù)的‘暫停'執(zhí)行?這里要引出Iterator接口(遍歷器)的概念
二.Iterator的概念
????Iterator是一種接口,它為不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator 接口,就可以完成遍歷操作。
????Iterator可以認(rèn)為是一個(gè)指針對(duì)象,通過next方法對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷,每次調(diào)用next方法,指針就指向數(shù)組的下一個(gè)成員并返回?cái)?shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。該信息是一個(gè)對(duì)象,包含value和done兩個(gè)屬性。其中,value屬性是當(dāng)前成員的值,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束。
????ES6規(guī)定,Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator屬性,調(diào)用這個(gè)接口,就會(huì)返回一個(gè)遍歷器對(duì)象。
下面用數(shù)組為栗子演示
let arr = [1, 2, 3];
// 返回遍歷器對(duì)象
let it = arr[Symbol.iterator]();
// 通過next方法遍歷
console.log(it.next()) // { value: 1, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }
????并不是所有的數(shù)據(jù)結(jié)構(gòu)都原生具備 Iterator 接口。ES6中原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下。
- Array
- Map
- Set
- String
- TypedArray
- 函數(shù)的 arguments 對(duì)象
- NodeList 對(duì)象
????Iterator 接口用于for...of循環(huán),也就是說,一個(gè)數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator 接口,他就可以被for...of遍歷。反之則無法遍歷(如object)。
????不過,我們可以給沒有原生Iterator 接口的數(shù)據(jù)結(jié)構(gòu)手動(dòng)部署該接口。具體來說,就是給其添加Symbol.iterator屬性,它是一個(gè)函數(shù),調(diào)用該函數(shù),返回遍歷器對(duì)象。這樣,我們用for...of對(duì)其遍歷時(shí),就會(huì)手動(dòng)調(diào)用我們部署的Iterator 接口。下面演示給object部署Iterator 接口。
const obj = {
a: 'a',
b: 'b',
c: 'c',
[Symbol.iterator]: function () {
let keys = Object.keys(this);
let index = 0;
return {
next: function () {
return index < keys.length
? {
value: this[keys[index++]],
done: false,
}
: {
value: undefined,
done: true,
};
}.bind(this),
};
},
};
for (const it of obj) {
console.log(it)
}
// a
// b
// c
????經(jīng)過上面討論我們知道,對(duì)于遍歷器,只有執(zhí)行next方法,才會(huì)繼續(xù)向下遍歷。Generator 函數(shù)正是利用這一點(diǎn),實(shí)現(xiàn)異步操作的同步化。進(jìn)一步講,執(zhí)行 Generator 函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象。它可以遍歷Generator 函數(shù)內(nèi)部封裝的多個(gè)狀態(tài)。下面具體分析。
三.Generator函數(shù)的形式與基本使用
1. Generator 函數(shù)的形式。
Generator函數(shù)有兩個(gè)區(qū)別于普通函數(shù)的明顯特征。
- function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào)。
- 函數(shù)體內(nèi)部使用yield表達(dá)式劃分不同部狀態(tài)。
function*gen(){
yield 1
yield 2
}
// 得到遍歷器對(duì)象
let g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: undefined, done: true }
2. yield表達(dá)式
????通過上面例子我們能看出,yield表達(dá)式就是用來劃分Generator 函數(shù)的各個(gè)狀態(tài),他可以理解為函數(shù)暫停的標(biāo)志。當(dāng)執(zhí)行next()方法,遇到y(tǒng)ield表達(dá)式時(shí),就暫停執(zhí)行后面的操作,并將yield表達(dá)式的值作為next方法返回的信息對(duì)象的value屬性值。下次調(diào)用next()方法,繼續(xù)執(zhí)行yield表達(dá)式后面的操作。這一點(diǎn)很重要,我們將利用這一點(diǎn)實(shí)現(xiàn)像操作同步那樣操作異步。
function*gen(){
yield 1+2
yield 2+3
}
// 得到遍歷器對(duì)象
let g = gen()
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: 5, done: false }
console.log(g.next()) // { value: undefined, done: true }
四.Generator函數(shù)的異步應(yīng)用
????我們已經(jīng)了解了Generator 函數(shù)的基本特點(diǎn),回到最開始的問題,如何實(shí)現(xiàn)異步操作的同步化。我們的需求是在異步操作結(jié)束后,再執(zhí)行后面的操作,而Generator 函數(shù)的特點(diǎn)是只有在執(zhí)行next方法后,函數(shù)從當(dāng)前狀態(tài)變?yōu)橄乱粻顟B(tài)。因此我們只需用yield,將每個(gè)異步操作劃為一個(gè)狀態(tài),這樣就可以保證遇到異步操作時(shí)函數(shù)暫停執(zhí)行。而在每個(gè)異步操作結(jié)束的時(shí),調(diào)用next方法,使得函數(shù)繼續(xù)執(zhí)行,這就實(shí)現(xiàn)了用同步操作的邏輯來操作異步。
????要實(shí)現(xiàn)上述,還需解決兩個(gè)問題。
1. 傳遞異步結(jié)果
????我們知道,異步操作之后的處理往往需要異步的返回結(jié)果,那么一個(gè)首要問題就是如何將異步返回結(jié)果傳遞出來。
????我們要明確一點(diǎn),yield表達(dá)式是沒有返回值的(undefinded),也就是說直接使用下面這種方式是行不通的。
function*gen(){
const res = yield async1()
yield async2(res)
}
????要傳遞結(jié)果,我們要借助next方法。next方法如果有入?yún)ⅲ搮?shù)會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值。
function*gen(){
const res1 = yield 1
const res2 = yield res1+1
yield res2+2
}
// 得到遍歷器對(duì)象
let g = gen()
console.log(g.next()) // { value: 1, done: false }
// next方法傳入3 認(rèn)為res1=3 3+1=4
console.log(g.next(3)) // { value: 4, done: false }
console.log(g.next(4)) // { value: 6, done: false }
因此,我們只需將異步的返回結(jié)果傳入next方法即可
2. 異步結(jié)束后調(diào)用next方法
????我們?nèi)粘?duì)異步的處理無非是回調(diào)函數(shù)和Promsie兩種方式,因此也就有兩種思路解決該問題。
2.1 基于回調(diào)函數(shù)的Generator異步流程處理
????我們只需在異步的回調(diào)函數(shù)中調(diào)用next方法,即可實(shí)現(xiàn)異步結(jié)束后繼續(xù)執(zhí)行Generator函數(shù)。
const async1 = () => {
setTimeout(() => {
// 執(zhí)行next方法 傳遞異步結(jié)果
g.next(1);
});
};
const async2 = (res) => {
setTimeout(() => {
console.log(res + " from async1");
g.next(2);
});
};
const async3 = (res) => {
setTimeout(() => {
console.log(res + " from async2");
});
};
function* gen() {
const res1 = yield async1();
const res2 = yield async2(res1);
yield async3(res2);
}
// 得到遍歷器對(duì)象
let g = gen();
g.next();
//1 from async1
//2 from async2
????上面的代碼基本實(shí)現(xiàn)了需求,我們發(fā)現(xiàn)Generator函數(shù)內(nèi)部的異步邏輯處理,如果去掉yield就基本和同步操作一樣了。
????不過,上面代碼的問題也很明顯,我們需要對(duì)每個(gè)異步的回調(diào)進(jìn)行處理。這樣是很低效的,因?yàn)槲覀儼l(fā)現(xiàn)在回調(diào)中做的其實(shí)是同一件事,即執(zhí)行next方法并傳入異步返回結(jié)果。我們?nèi)绻軐⑦@個(gè)過程抽離出來,并自動(dòng)執(zhí)行。將使得代碼邏輯大為簡(jiǎn)化。下面依次解決這兩個(gè)問題。
- 抽取回調(diào)函數(shù)的處理
????如何能將回調(diào)函數(shù)的處理抽離出來?
????以setTimeot函數(shù)為例,它接受兩個(gè)參數(shù),分別是回調(diào)函數(shù)和延時(shí)時(shí)間。而我們希望將這個(gè)兩個(gè)參數(shù)分開傳入,單獨(dú)處理。這里就可以想到前面討論過的柯里化函數(shù)。柯里化函數(shù)可以將接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù),并返回接受余下參數(shù)的函數(shù)。
????還是以setTimeot函數(shù)為例,如果經(jīng)過柯里化處理,我們可以先傳入延時(shí)時(shí)間,再向返回的函數(shù)中傳入回調(diào),這就實(shí)現(xiàn)了上述需求。像下面這樣
function currying(time) {
return (cb) => {
return setTimeout(cb, time);
};
}
const curryTimeout = currying(500);
curryTimeout(() => {
console.log("timeOut");
});
????接下來的問題是,在什么地方處理異步回調(diào)。我們知道,next方法返回值的value屬性,就是yield表達(dá)式的執(zhí)行結(jié)果。我們?nèi)绻趛ield后面執(zhí)行經(jīng)過柯里化處理過的異步(如上例中的currying(500)),就會(huì)使得next方法返回值的value屬性是一個(gè)函數(shù),可以傳入異步的回調(diào)。因此我們只需將回調(diào)函數(shù)傳入next方法的value屬性即可。下面就基于上述對(duì)上例進(jìn)行改造。
function currying(time) {
return (cb) => {
return setTimeout(cb, time);
};
}
function* gen() {
const res1 = yield currying(500);
const res2 = yield currying(res1);
yield currying(res2);
}
const g = gen();
g.next().value(() => {
console.log("async1");
g.next(500).value(() => {
console.log("async2");
g.next(500).value(() => {
console.log("async3");
});
});
});
//每隔0.5秒依次打印async1 async2 async3
????可以看到代碼邏輯清晰了很多。這里還要說明一點(diǎn),事實(shí)上前面所謂的經(jīng)過柯里化處理的異步,就是Thunk 函數(shù)。所謂的Thunk 函數(shù),其實(shí)就是一個(gè)臨時(shí)函數(shù),它可以把一個(gè)多參數(shù)函數(shù),替換成一個(gè)只接受回調(diào)函數(shù)作為參數(shù)的單參數(shù)函數(shù)。如上例中的curryTimeout函數(shù),它就是一個(gè)只接受回調(diào)函數(shù)作為參數(shù)的中間函數(shù),也就是Thunk 函數(shù)。用阮一峰老師的話說就是:任何函數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成 Thunk 函數(shù)的形式。上面的例子相當(dāng)于手動(dòng)實(shí)現(xiàn)了一個(gè)丐版Thunk 函數(shù)轉(zhuǎn)換器,生產(chǎn)環(huán)境中一般使用nodejs的Thunkify模塊,它可以實(shí)現(xiàn)Thunk 函數(shù)的轉(zhuǎn)換。
????接下來要做的就是變手動(dòng)執(zhí)行為自動(dòng)執(zhí)行。
- 自動(dòng)執(zhí)行
????仔細(xì)觀察手動(dòng)執(zhí)行Generator 函數(shù)的代碼會(huì)返現(xiàn),我們做的其實(shí)只有一件事,即把同一個(gè)回調(diào)傳入next方法的value屬性,而回調(diào)要做的就是執(zhí)行next方法并傳遞異步結(jié)果。
????基于上述,我們可以實(shí)現(xiàn)Generator 函數(shù)按照既定邏輯自動(dòng)執(zhí)行的程序。它只需判斷next方法返回值的done屬性,只要不為true,就一直將回調(diào)傳入next方法的value屬性。
????下面用node.js fs模塊的readFileAPI演示,使用thunkify模塊將異步API轉(zhuǎn)換為Thunk函數(shù)。準(zhǔn)備兩個(gè)文本文件,內(nèi)容分別是'對(duì)酒當(dāng)歌' '人生幾何'。
const thunkify = require("thunkify");
const fs = require("fs");
const readFileThunk = thunkify(fs.readFile)
function* gen() {
yield readFileThunk("./text1.txt");
yield readFileThunk("./text2.txt");
}
function run(fn) {
const gen = fn();
function next(err, data) {
// 錯(cuò)誤優(yōu)先的回調(diào)
if (data) console.log(data.toString());
const res = gen.next(data);
if (res.done) return;
res.value(next);
}
next();
}
run(gen);
// 對(duì)酒當(dāng)歌
// 人生幾何
????可以看到,有了自動(dòng)執(zhí)行器,我們只管在Generator函數(shù)內(nèi)部處理異步,最后直接把 Generator 函數(shù)傳入run函數(shù)即可,當(dāng)然前提是yield表達(dá)式必須是Thunk函數(shù)。
2.2 基于Promise的Generator異步流程處理
????通過觀察前面實(shí)現(xiàn)的基于回調(diào)的Generator函數(shù)自動(dòng)執(zhí)行器不難看出,自動(dòng)執(zhí)行的關(guān)鍵其實(shí)就是在異步結(jié)束后調(diào)用next方法,讓Generator函數(shù)繼續(xù)執(zhí)行。同樣,利用Promise.then方法也能做到這一點(diǎn)。
????沿用上面例子對(duì)其進(jìn)行改造,我們要做的其實(shí)很簡(jiǎn)單
- 將readFile函數(shù)包裝為Promise
- 利用Promise.then方法自動(dòng)執(zhí)行
const fs = require("fs");
function promisify_readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
function* gen() {
yield promisify_readFile("./text1.txt");
yield promisify_readFile("./text2.txt");
}
function run(fn) {
const gen = fn();
function next(data) {
if (data) console.log(data.toString());
const res = gen.next(data);
if (res.done) return;
//res.value返回的是Promise,可以通過then方法繼續(xù)執(zhí)行Generator
res.value.then(next,(r)=>console.log(r))
}
next();
}
run(gen);
????至此我們已經(jīng)基本實(shí)現(xiàn)了像文章開頭的需求,并實(shí)現(xiàn)了自動(dòng)執(zhí)行,其實(shí)這就是著名的co模塊的核心實(shí)現(xiàn)原理。
五.co模塊及其實(shí)現(xiàn)原理
????co模塊一個(gè)著名的用于Generator函數(shù)自動(dòng)執(zhí)行的模塊。它的使用非常簡(jiǎn)單,只需將Generator函數(shù)傳入co,即可自動(dòng)執(zhí)行。
const co = require("co");
function* gen() {
const res1 = yield promisify_readFile("./text1.txt");
console.log(res1.toString())
const res2 = yield promisify_readFile("./text2.txt");
console.log(res2.toString())
}
co(gen)
// 對(duì)酒當(dāng)歌
// 人生幾何
實(shí)現(xiàn)原理
????其實(shí),經(jīng)過上面對(duì)Generator函數(shù)自動(dòng)執(zhí)行的討論我們能夠知道,co模塊核心實(shí)現(xiàn)原理就是我們實(shí)現(xiàn)的run函數(shù)的擴(kuò)展。具體如下
- co返回的是Promise 對(duì)象,因此要添加一些改變Promise狀態(tài)的邏輯
- 要確保每一步的返回值都是Promise
下面實(shí)現(xiàn)一個(gè)丐版的co模塊
function co(gen) {
return new Promise(function (resolve, reject) {
gen = gen();
if (!gen || typeof gen.next !== "function") return resolve(gen);
function next(data) {
const res = gen.next(data);
if (res.done) {
return resolve(res.value);
} else {
// 確保每一步的返回值都是Promise
const value = Promise.resolve(res.value);
value.then(next, (r) => reject(r));
}
}
next();
});
}
// 由于co返回的是Promise,因此可以指定then方法使得
// 在Generator執(zhí)行完成后進(jìn)行一些操作
co(gen).then(()=>console.log('end'))
// 對(duì)酒當(dāng)歌
// 人生幾何
// end
????co模塊是async/await關(guān)鍵字的前身,async/await被譽(yù)為異步編程的終極解決方案,后面會(huì)著重介紹。