JavaScript 異步編程指南 — 了解下 Generator 更好的掌握異步編程
Generator 是 ES6 對協(xié)程的實(shí)現(xiàn),提供了一種異步編程的解決方案,和 Promise 一樣都是線性的模式,相比 Promise 在復(fù)雜的業(yè)務(wù)場景下避免了 .then().then() 這樣的代碼冗余。
曾經(jīng)一直認(rèn)為 Generator 是一種過渡的解決方案,并沒有過多的去了解它,后來在一些項(xiàng)目中還會看到它的身影,基于它還可以做很多有意思的事情,在不了解的情況下,你無法準(zhǔn)確預(yù)知它的一些行為能夠?qū)е率裁磫栴}。
例如,Node.js 的可讀流對象在 v10.0.0 版本已試驗(yàn)性的支持了異步迭代器,當(dāng)監(jiān)聽來自可讀流的數(shù)據(jù)時無需在基于事件和回調(diào)的方式 on('data', callback),可以方便的使用 for...await...of 異步迭代,看過源碼會發(fā)現(xiàn)在它的內(nèi)部實(shí)現(xiàn)中是用的異步生成器函數(shù)來生成的異步迭代器。還有目前的 Async/Await 是一種更好的異步解決方案,在下一節(jié)我們會講,本質(zhì)上還是基于 Generator 的語法糖。
如果想更好的理解 JavaScript 的異步編程,學(xué)習(xí)下 Generator 是沒錯的~
基本使用
Generator 函數(shù)聲明
形式上 Generator 函數(shù)與普通函數(shù)沒太大區(qū)別,兩個特點(diǎn):一是 function 關(guān)鍵字與函數(shù)名之間使用 * 號表達(dá),二是內(nèi)部使用使用 yield 表達(dá)式。
function *test() {
yield 'A';
yield 'B';
yield 'C';
}
next()
如果是普通函數(shù),當(dāng) test() 后函數(shù)會立即執(zhí)行,而生成器函數(shù)調(diào)用后函數(shù)不會立即執(zhí)行,會給我們返回一個迭代器對象。
調(diào)用 next() 從函數(shù)頭部或上一次暫停的地方執(zhí)行,直到遇到下一個 yield 表達(dá)式暫停或 return 終止,當(dāng)遇到 yield 表達(dá)式暫停后,想要繼續(xù)執(zhí)行下去,需接著調(diào)用 next() 恢復(fù)執(zhí)行。
next() 返回 yield 表達(dá)式值,當(dāng) done 為 true 時迭代完成。
const gen = test();
gen.next() // { value: 'A', done: false }
gen.next() // { value: 'B', done: false }
gen.next() // { value: 'C', done: false }
gen.next() // { value: undefined, done: false }
return()
使用 return() 方法返回給定的值,可以強(qiáng)行終止,即使生成器還沒有運(yùn)行完畢。
const gen = test();
gen.next() // { value: 'A', done: false }
gen.next() // { value: 'B', done: false }
gen.return('termination'); // { value: 'termination', done: true }
gen.next() // { value: undefined, done: true }
gen.return() 相當(dāng)于將 yield 語句替換為了 return 表達(dá)式 **yield 'B' 相當(dāng)于 return 'termination'**。
throw()
生成器函數(shù)返回的迭代器對象還有一個 throw() 方法,在函數(shù)體外拋出錯誤,在函數(shù)體內(nèi)捕獲。需要注意 throw() 方法拋出的錯誤要被內(nèi)部捕獲,必須至少執(zhí)行過一次 next() 方法。
function *test() {
yield 'A';
try {
yield 'B';
} catch (err) {
console.error('內(nèi)部錯誤', err); // 內(nèi)部錯誤 unknown mistake
}
yield 'C';
}
const gen = test();
gen.next()
gen.next() // { value: 'B', done: false }
gen.throw('unknown mistake')
console.log(gen.next()); // { value: undefined, done: true }
gen.throw() 相當(dāng)于將 yield 語句替換為了 throw 表達(dá)式 **yield 'B' 相當(dāng)于 throw 'unknown mistake'**。
再看 yield 表達(dá)式與 next 方法
yield 表達(dá)式本身自己沒有值,返回 undefined,可以通過 next() 方法將上一個 yield 表達(dá)式的值做為參數(shù)傳入。
下面我們將上面示例改下每一個 yiled 依賴前一個 yield 表達(dá)式的返回值。
function *test() {
const res1 = yield 'A';
const res2 = yield res1 + 'B';
const res3 = yield res2 + 'C';
return res3;
}
如果按照上面 gen.next() 不傳入?yún)?shù),結(jié)果只會拿到 undefined。
以下第一次調(diào)用 gen2.next() 拿到返回值為 A,第二次調(diào)用 next() 時傳入第一次的返回值,test() 函數(shù)內(nèi)部 res1 就可取到第一次 yield 表達(dá)式的值,后面執(zhí)行一樣。
因?yàn)?next() 傳入的是第一次 yield 表達(dá)式的返回值,所以第一次在調(diào)用 next() 方法時無需傳入?yún)?shù)。
const gen2 = test();
const res1 = gen2.next(); // { value: 'A', done: false }
const res2 = gen2.next(res1.value) // { value: 'AB', done: false }
const res3 = gen2.next(res2.value) // { value: 'ABC', done: false }
const res = gen2.next(res3.value); // { value: 'ABC', done: true }
console.log(res);
gen.next 相當(dāng)于將 yield 語句替換為了一個表達(dá)式值,例如 gen.next('A') 可以這樣理解 const res2 = yield res1 + 'B'** 相當(dāng)于 **const res2 = 'A' + 'B'。
Generator 與迭代器
迭代器是通過 next() 方法實(shí)現(xiàn)可迭代協(xié)議的任何一個對象,該方法返回 value 和 done 兩個屬性,其中 value 屬性是當(dāng)前成員的值,done 屬性表示遍歷是否結(jié)束。
生成器函數(shù)在最初調(diào)用時會返回一種稱為 Generator 的迭代器,這樣可以通過 for...of 遍歷。
function *test() {
yield 'A';
yield 'B';
yield 'C';
return 'D';
}
const gen = test();
for (const item of gen) {
console.log(item); // A B C
}
有個點(diǎn)需要注意下,for...of 只遍歷到最后一個 yield 關(guān)鍵字,最后一個 return 'D' 忽略掉了,如果使用 next() 是會處理 return 語句的。
實(shí)例:Generator + 狀態(tài)機(jī)
Generator 用于實(shí)現(xiàn)狀態(tài)機(jī)還是比較簡單的,也是 JavaScript 里面高級的用法。例如,我們使用 A、B、C 三種狀態(tài)去描述一個事物,狀態(tài)之間是一種有序循環(huán)的,總是 A-B-C-A-B... 永遠(yuǎn)跑不出第 4 種狀態(tài)。
const state = function* (){
while(1){
yield 'A';
yield 'B';
yield 'C';
}
}
const status = state();
setInterval(() => {
console.log(status.next().value) // A B C A B C A B...
}, 1000)
實(shí)例:Generator + Promise
在 Promise 小節(jié)中我們基于 Promise 做了一次改造,你可以回頭去看下,下面我們使用 Generator 改造后看下差別是什么?
下例,去掉 yield 關(guān)鍵字和我們使用正常的普通函數(shù)沒什么區(qū)別,為了使 Generator 迭代器對象能夠自動執(zhí)行,還要借助外部模塊 co 實(shí)現(xiàn)。
co(function *() {
const files = yield fs.readdir(rootDir);
for (const filname of files) {
const file = path.resolve(rootDir, filname);
const stats = yield fs.lstat(file);
if (stats.isFile()) {
const chunk = yield fs.readFile(file);
console.log(chunk.toString());
}
}
});
總結(jié)
生成器是一個強(qiáng)大的通用控制結(jié)構(gòu),不像普通函數(shù)那樣調(diào)用之后就直接運(yùn)行到結(jié)束,在程序運(yùn)行過程中當(dāng)遇到 yield 關(guān)鍵字它可以使其保持暫停狀態(tài),直到將來某個時間點(diǎn)繼續(xù)恢復(fù)執(zhí)行。
在 ES6 中它的最大價值就是管理我們的異步代碼,但是還不是很完美,我們不得不借助類似與 co 這樣的工具來使我們的生成器函數(shù)自動調(diào)用 next() 方法運(yùn)行。不過,在 ES7 到來之后,這一切都過去了,通過 Async/Await 可以更好的管理我們的異步任務(wù)。
往期回顧
·END·
匯聚精彩的免費(fèi)實(shí)戰(zhàn)教程
喜歡本文,點(diǎn)個“在看”告訴我
