JavaScript?Generator異步過(guò)度的實(shí)現(xiàn)詳解
異步過(guò)渡方案Generator
在使用 Generator 前,首先知道 Generator 是什么。
如果讀者有 Python 開(kāi)發(fā)經(jīng)驗(yàn),就會(huì)發(fā)現(xiàn),無(wú)論是概念還是形式上,ES2015 中的 Generator 幾乎就是 Python 中 Generator 的翻版。
Generator 本質(zhì)上是一個(gè)函數(shù),它最大的特點(diǎn)就是可以被中斷,然后恢復(fù)執(zhí)行。通常來(lái)說(shuō),當(dāng)開(kāi)發(fā)者調(diào)用一個(gè)函數(shù)之后,這個(gè)函數(shù)的執(zhí)行就脫離了開(kāi)發(fā)者的控制,只有函數(shù)執(zhí)行完畢之后,控制權(quán)才能重新回到調(diào)用者手中,因此程序員在編寫(xiě)方法代碼時(shí),唯一
能夠影響方法執(zhí)行的只有預(yù)先定義的 return 關(guān)鍵字。
Promise 也是如此,我們也無(wú)法控制 Promise 的執(zhí)行,新建一個(gè) Promise 后,其狀態(tài)自動(dòng)轉(zhuǎn)換為 pending,同時(shí)開(kāi)始執(zhí)行,直到狀態(tài)改變后我們才能進(jìn)行下一步操作。
而 Generator 函數(shù)不同,Generator 函數(shù)可以由用戶(hù)執(zhí)行中斷或者恢復(fù)執(zhí)行的操作,Generator 中斷后可以轉(zhuǎn)去執(zhí)行別的操作,然后再回過(guò)頭從中斷的地方恢復(fù)執(zhí)行。
1. Generator 的使用
Generator 函數(shù)和普通函數(shù)在外表上最大的區(qū)別有兩個(gè):
- 在
function關(guān)鍵字和方法名中間有個(gè)星號(hào)(*)。 - 方法體中使用
yield關(guān)鍵字。
function* Generator() {
yield "Hello World";
return "end";
}
和普通方法一樣,Generator 可以定義成多種形式:
// 普通方法形式
function* generator() {}
//函數(shù)表達(dá)式
const gen = function* generator() {}
// 對(duì)象的屬性方法
const obi = {
* generator() {
}
}Generator 函數(shù)的狀態(tài)
yield 關(guān)鍵字用來(lái)定義函數(shù)執(zhí)行的狀態(tài),在前面代碼中,如果 Generator 中定義了 x 個(gè) yield 關(guān)鍵字,那么就有 x + 1 種狀態(tài)(+1是因?yàn)樽詈蟮?return 語(yǔ)句)。
2. Generator 函數(shù)的執(zhí)行
跟普通函數(shù)相比,Generator 函數(shù)更像是一個(gè)類(lèi)或者一種數(shù)據(jù)類(lèi)型,以下面的代碼為例,直接執(zhí)行一個(gè) Generator 會(huì)得到一個(gè) Generator 對(duì)象,而不是執(zhí)行方法體中的內(nèi)容。
const gen = Generator();
按照通常的思路,gen 應(yīng)該是 Generator() 函數(shù)的返回值,上面也提到Generator 函數(shù)可能有多種狀態(tài),讀者可能會(huì)因此聯(lián)想到 Promise,一個(gè) Promise 也可能有三種狀態(tài)。不同的是 Promise 只能有一個(gè)確定的狀態(tài),而 Generator 對(duì)象會(huì)逐個(gè)經(jīng)歷所有的狀態(tài),直到 Generator 函數(shù)執(zhí)行完畢。
當(dāng)調(diào)用 Generator 函數(shù)之后,該函數(shù)并沒(méi)有立刻執(zhí)行,函數(shù)的返回結(jié)果也不是字符串,而是一個(gè)對(duì)象,可以將該對(duì)象理解為一個(gè)指針,指向 Generator 函數(shù)當(dāng)前的狀態(tài)。(為了便于說(shuō)明,我們下面采用指針的說(shuō)法)。
當(dāng) Generator 被調(diào)用后,指針指向方法體的開(kāi)始行,當(dāng) next 方法調(diào)用后,該指針向下移動(dòng),方法也跟著向下執(zhí)行,最后會(huì)停在第一個(gè)遇到的 yield 關(guān)鍵字前面,當(dāng)再次調(diào)用 next 方法時(shí),指針會(huì)繼續(xù)移動(dòng)到下一個(gè) yield 關(guān)鍵字,直到運(yùn)行到方法的最后一行,以下面代碼為例,完整的執(zhí)行代碼如下:
function* Generator() {
yield "Hello World";
return "end";
}
const gen = Generator();
console.log(gen.next()); // { value: 'Hello World', done: false }
console.log(gen.next()); // { value: 'end', done: true }
console.log(gen.next()); // { value: undefined, done: true }上面的代碼一共調(diào)用了三次 next 方法,每次都返回一個(gè)包含執(zhí)行信息的對(duì)象,包含一個(gè)表達(dá)式的值和一個(gè)標(biāo)記執(zhí)行狀態(tài)的 flag。
第一次調(diào)用 next 方法,遇到一個(gè) yield 語(yǔ)句后停止,返回對(duì)象的 value 的值就是 yield 語(yǔ)句的值,done 屬性用來(lái)標(biāo)志 Generator 方法是否執(zhí)行完畢。
第二次調(diào)用 next 方法,程序執(zhí)行到 return 語(yǔ)句的位置,返回對(duì)象的 value 值即為 return 語(yǔ)句的值,如果沒(méi)有 return 語(yǔ)句,則會(huì)一直執(zhí)行到函數(shù)結(jié)束,value 值為 undefined,done 屬性值為 true。
第三次調(diào)用 next 方法時(shí),Generator 已經(jīng)執(zhí)行完畢,因此 value 的值為undefined。
2.1 yield 關(guān)鍵字
yield 本意為 生產(chǎn) ,在 Python、Java 以及 C# 中都有 yield 關(guān)鍵字,但只有Python 中 yield 的語(yǔ)義相似(理由前面也說(shuō)了)。
當(dāng) next 方法被調(diào)用時(shí),Generator 函數(shù)開(kāi)始向下執(zhí)行,遇到 yield 關(guān)鍵字時(shí),會(huì)暫停當(dāng)前操作,并且對(duì) yield 后的表達(dá)式進(jìn)行求值,無(wú)論 yield 后面表達(dá)式返回的是何種類(lèi)型的值,yield 操作最后返回的都是一個(gè)對(duì)象,該對(duì)象有 value 和 done 兩個(gè)屬性。
value 很好理解,如果后面是一個(gè)基本類(lèi)型,那么 value 的值就是對(duì)應(yīng)的值,更為常見(jiàn)的是 yield 后面跟的是 Promise 對(duì)象。
done 屬性表示當(dāng)前 Generator 對(duì)象的狀態(tài),剛開(kāi)始執(zhí)行時(shí) done 屬性的值為false,當(dāng) Generator 執(zhí)行到最后一個(gè) yield 或者 return 語(yǔ)句時(shí),done 的值會(huì)變成 true,表示 Generator 執(zhí)行結(jié)束。
注意:yield關(guān)鍵字本身不產(chǎn)生返回值。例如下面的代碼:
function* foo(x) {
const y = yield(x + 1);
return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: undefined, done: true }為什么第二個(gè) next 方法執(zhí)行后,y 的值卻是 undefined。
實(shí)際上,我們可以做如下理解:next 方法的返回值是 yield 關(guān)鍵字后面表達(dá)式的值,而 yield 關(guān)鍵字本身可以視為一個(gè)不產(chǎn)生返回值的函數(shù),因此 y 并沒(méi)有被賦值。上面的例子中如果要計(jì)算 y 的值,可以將代碼改成:
function* foo(x) {
let y;
yield y = x + 1;
return 'end';
}
next 方法還可以接受一個(gè)數(shù)值作為參數(shù),代表上一個(gè) yield 求值的結(jié)果。
function* foo(x) {
const y = yield(x + 1);
return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next(10)); // { value: 10, done: true }上面的代碼等價(jià)于:
function* foo(x) {
let y = yield(x + 1);
y = 10;
return y;
}
const gen = foo(5);
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: 10, done: true }next 可以接收參數(shù)代表可以從外部傳一個(gè)值到 Generator 函數(shù)內(nèi)部,乍一看沒(méi)有什么用處,實(shí)際上正是這個(gè)特性使得 Generator 可以用來(lái)組織異步方法,我們會(huì)在后面介紹。
2.2 next 方法與 Iterator 接口
一個(gè) Iterator 同樣使用 next 方法來(lái)遍歷元素。由于 Generator 函數(shù)會(huì)返回一個(gè)對(duì)象,而該對(duì)象實(shí)現(xiàn)了一個(gè) Iterator 接口,因此所有能夠遍歷 Iterator 接口的方法都可以用來(lái)執(zhí)行 Generator,例如 for/of、aray.from()等。
可以使用 for/of 循環(huán)的方式來(lái)執(zhí)行 Generator 函數(shù)內(nèi)的步驟,由于 for/of 本身就會(huì)調(diào)用 next 方法,因此不需要手動(dòng)調(diào)用。
注意:循環(huán)會(huì)在 done 屬性為 true 時(shí)停止,以下面的代碼為例,最后的 'end' 并不會(huì)被打印出來(lái),如果希望被打印,需要將最后的 return 改為 yield。
function* Generator() {
yield "Hello Node";
yield "From Lear"
return "end"
}
const gen = Generator();
for (let i of gen) {
console.log(i);
}
// 和 for/of 循環(huán)等價(jià)
console.log(Array.from(Generator()));;前面提到過(guò),直接打印 Generator 函數(shù)的示例沒(méi)有結(jié)果,但既然 Generator 函數(shù)返回了一個(gè)遍歷器,那么就應(yīng)該具有 Symbol.iterator 屬性。
console.log(gen[Symbol.iterator]);
// 輸出:[Function: [Symbol.iterator]]
3. Generator 中的錯(cuò)誤處理
Generator 函數(shù)的原型中定義了 throw 方法,用于拋出異常。
function* generator() {
try {
yield console.log("Hello");
} catch (e) {
console.log(e);
}
yield console.log("Node");
return "end";
}
const gen = generator();
gen.next();
gen.throw("throw error");
// 輸出
// Hello
// throw error
// Node
上面代碼中,執(zhí)行完第一個(gè) yield 操作后,Generator 對(duì)象拋出了異常,然后被函數(shù)體中 try/catch 捕獲。當(dāng)異常被捕獲后,Generator 函數(shù)會(huì)繼續(xù)向下執(zhí)行,直到遇到下一個(gè) yield 操作并輸出 yield 表達(dá)式的值。
function* generator() {
try {
yield console.log("Hello World");
} catch (e) {
console.log(e);
}
console.log('test');
yield console.log("Node");
return "end";
}
const gen = generator();
gen.next();
gen.throw("throw error");
// 輸出
// Hello World
// throw error
// test
// Node
如果 Generator 函數(shù)在執(zhí)行的過(guò)程中出錯(cuò),也可以在外部進(jìn)行捕獲。
function* generator() {
yield console.log(undefined, undefined);
return "end";
}
const gen = generator();
try {
gen.next();
} catch (e) {
}Generator 的原型對(duì)象還定義了 return() 方法,用來(lái)結(jié)束一個(gè) Generator 函數(shù)的執(zhí)行,這和函數(shù)內(nèi)部的 return 關(guān)鍵字不是一個(gè)概念。
function* generator() {
yield console.log('Hello World');
yield console.log('Hello 夏安');
return "end";
}
const gen = generator();
gen.next(); // Hello World
gen.return();
// return() 方法后面的 next 不會(huì)被執(zhí)行
gen.next();4. 用 Generator 組織異步方法
我們之所以可以使用 Generator 函數(shù)來(lái)處理異步任務(wù),原因有二:
Generator函數(shù)可以中斷和恢復(fù)執(zhí)行,這個(gè)特性由yield關(guān)鍵字來(lái)實(shí)現(xiàn)。Generator函數(shù)內(nèi)外可以交換數(shù)據(jù),這個(gè)特性由next函數(shù)來(lái)實(shí)現(xiàn)。
概括一下 Generator 函數(shù)處理異步操作的核心思想:先將函數(shù)暫停在某處,然后拿到異步操作的結(jié)果,然后再把這個(gè)結(jié)果傳到方法體內(nèi)。
yield 關(guān)鍵字后面除了通常的函數(shù)表達(dá)式外,比較常見(jiàn)的是后面跟的是一個(gè) Promise,由于 yield 關(guān)鍵字會(huì)對(duì)其后的表達(dá)式進(jìn)行求值并返回,那么調(diào)用 next 方法時(shí)就會(huì)返回一個(gè) Promise 對(duì)象,我們可以調(diào)用其 then 方法,并在回調(diào)中使用 next 方法將結(jié)果傳回 Generator。
function* gen() {
const result = yield readFile_promise("foo.txt");
console.log(result);
}
const g = gen();
const result = g.next();
result.value.then(function (data) {
g.next(data);
});
上面的代碼中,Generator 函數(shù)封裝了 readFile_promise 方法,該方法返回一個(gè) Promise,Generator 函數(shù)對(duì) readFile_promise 的調(diào)用方式和同步操作基本相同,除了 yield 關(guān)鍵字之外。
上面的 Generator 函數(shù)中只有一個(gè)異步操作,當(dāng)有多個(gè)異步操作時(shí),就會(huì)變成下面的形式。
function* gen() {
const result = yield readFile_promise("foo.txt");
console.log(result);
const result2 = yield readFile_promise("bar.txt");
console.log(result2);
}
const g = gen();
const result = g.next();
result.value.then(function (data) {
g.next(data).value.then(function (data) {
g.next(data);
})
});然而看起來(lái)還是嵌套的回調(diào)?難道使用 Generator 的初衷不是優(yōu)化嵌套寫(xiě)法嗎?說(shuō)的沒(méi)錯(cuò),雖然在調(diào)用時(shí)保持了同步形式,但我們需要手動(dòng)執(zhí)行 Generator 函數(shù),于是在執(zhí)行時(shí)又回到了嵌套調(diào)用。這是 Generator 的缺點(diǎn)。
5. Generator 的自動(dòng)執(zhí)行
對(duì) Generator 函數(shù)來(lái)說(shuō),我們也看到了要順序地讀取多個(gè)文件,就要像上面代碼那樣寫(xiě)很多用來(lái)執(zhí)行的代碼。無(wú)論是 Promise 還是 Generator,就算在編寫(xiě)異步代碼時(shí)能獲得便利,但執(zhí)行階段卻要寫(xiě)更多的代碼,Promise 需要手動(dòng)調(diào)用 then 方法,Generator 中則是手動(dòng)調(diào)用 next 方法。
當(dāng)需要順序執(zhí)行異步操作的個(gè)數(shù)比較少的情況下,開(kāi)發(fā)者還可以接受手動(dòng)執(zhí)行,但如果面對(duì)多個(gè)異步操作就有些難辦了,我們避免了回調(diào)地獄,卻又陷到了執(zhí)行地獄里面。我們不會(huì)是第一個(gè)遇到自動(dòng)執(zhí)行問(wèn)題的人,社區(qū)已經(jīng)有了很多解決方案,但為了更深入地了解 Promise 和 Generator,我們不妨先試著獨(dú)立地解決這個(gè)問(wèn)題,如何能夠讓一個(gè) Generator 函數(shù)自動(dòng)執(zhí)行?
5.1 自動(dòng)執(zhí)行器的實(shí)現(xiàn)
既然 Generator 函數(shù)是依靠 next 方法來(lái)執(zhí)行的,那么我們只要實(shí)現(xiàn)一個(gè)函數(shù)自動(dòng)執(zhí)行 next 方法不就可以了嗎,針對(duì)這種思路,我們先試著寫(xiě)出這樣的代碼:
function auto(generator) {
const gen = generator();
while (gen.next().value !== undefined) {
gen.next();
}
}
思路雖然沒(méi)錯(cuò),但這種寫(xiě)法并不正確,首先這種方法只能用在最簡(jiǎn)單的 Generator 函數(shù)上,例如下面這種:
function* generator() {
yield 'Hello World';
return 'end';
}
另一方面,由于 Generator 沒(méi)有 hasNext 方法,在 while 循環(huán)中作為條件的:gen.next().value !== undefined 在第一次條件判斷時(shí)就開(kāi)始執(zhí)行了,這表示我們拿不到第一次執(zhí)行的結(jié)果。因此這種寫(xiě)法行不通。
那么換一種思路,我們前面介紹了 for/of 循環(huán),那么也可以用它來(lái)執(zhí)行 Generator。
function* Generator() {
yield "Hello World";
yield "Hello 夏安";
yield "end";
}
const gen = Generator();
for (let i of gen) {
console.log(i);
}
// 輸出結(jié)果
// Hello World
// Hello 夏安
// end
看起來(lái)沒(méi)什么問(wèn)題了,但同樣地也只能拿來(lái)執(zhí)行最簡(jiǎn)單的 Generator 函數(shù),然而我們的主要目的還是管理異步操作。
5.2 基于Promise的執(zhí)行器
前面實(shí)現(xiàn)的執(zhí)行器都是針對(duì)普通的 Generator 函數(shù),即里面沒(méi)有包含異步操作,在實(shí)際應(yīng)用中,yield 后面跟的大都是 Promise,這時(shí)候 for/of 實(shí)現(xiàn)的執(zhí)行器就不起作用了。
通過(guò)觀(guān)察,我們發(fā)現(xiàn) Generator 的嵌套執(zhí)行是一種遞歸調(diào)用,每一次的嵌套的返回結(jié)果都是一個(gè) Promise 對(duì)象。
const g = gen();
const result = g.next();
result.value.then(function (data) {
g.next(data).value.then(function (data) {
g.next(data);
})
});
那么,我們可以根據(jù)這個(gè)寫(xiě)出新的執(zhí)行函數(shù)。
function autoExec(gen) {
function next(data) {
const result = gen.next(data);
// 判斷執(zhí)行是否結(jié)束
if (result.done) return result.value;
result.value.then(function (data) {
next(data);
});
}
next();
}這個(gè)執(zhí)行器因?yàn)檎{(diào)用了 then 方法,因此只適用于 yield 后面跟一個(gè) Promise 的方法。
5.3 使用 co 模塊來(lái)自動(dòng)執(zhí)行
為了解決 generator 執(zhí)行的問(wèn)題,TJ 于2013年6月發(fā)布了著名 co 模塊,這是一個(gè)用來(lái)自動(dòng)執(zhí)行 Generator 函數(shù)的小工具,和 Generator 配合可以實(shí)現(xiàn)接近同步的調(diào)用方式,co 方法仍然會(huì)返回一個(gè) Promise。
const co = require("co");
function* gen() {
const result = yield readFilePromise("foo.txt");
console.log(result);
const result2 = yield readFilePromise("bar.txt");
console.log(result2);
}
co(gen);只要將 Generator 函數(shù)作為參數(shù)傳給 co 方法就能將內(nèi)部的異步任務(wù)順序執(zhí)行,要使用 co 模塊,yield 后面的語(yǔ)句只能是 promsie 對(duì)象。
到此為止,我們對(duì)異步的處理有了一個(gè)比較妥當(dāng)?shù)姆绞剑?generator+co,我們基本可以用同步的方式來(lái)書(shū)寫(xiě)異步操作了。但 co 模塊仍有不足之處,由于它仍然返回一個(gè) Promise,這代表如果想要獲得異步方法的返回值,還要寫(xiě)成下面這種形式:
co(gen).then(function (value) {
console.log(value);
});
另外,當(dāng)面對(duì)多個(gè)異步操作時(shí),除非將所有的異步操作都放在一個(gè) Generator 函數(shù)中,否則如果需要對(duì) co 的返回值進(jìn)行進(jìn)一步操作,仍然要將代碼寫(xiě)到 Promise 的回調(diào)中去。
到此這篇關(guān)于JavaScript Generator異步過(guò)度的實(shí)現(xiàn)詳解的文章就介紹到這了,更多相關(guān)JavaScript Generator 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JavaScript前端迭代器Iterator與生成器Generator講解
- Javascript生成器(Generator)的介紹與使用
- JS Generator 函數(shù)的含義與用法實(shí)例總結(jié)
- JS為什么說(shuō)async/await是generator的語(yǔ)法糖詳解
- js中Generator函數(shù)的深入講解
- JavaScript 中使用 Generator的方法
- js使用generator函數(shù)同步執(zhí)行ajax任務(wù)
- 深入理解js generator數(shù)據(jù)類(lèi)型
- JavaScript Generator函數(shù)使用分析
相關(guān)文章
js的隱含參數(shù)(arguments,callee,caller)使用方法
本篇文章只要是對(duì)js的隱含參數(shù)(arguments,callee,caller)使用方法進(jìn)行了介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2014-01-01
javascript實(shí)現(xiàn)文本框標(biāo)簽驗(yàn)證的實(shí)例代碼
這篇文章主要介紹了javascript實(shí)現(xiàn)文本框標(biāo)簽驗(yàn)證的實(shí)例代碼,需要的朋友可以參考下2018-10-10
JavaScript?評(píng)測(cè)代碼運(yùn)行速度的案例代碼
在?JavaScript?中,可以使用?performance.now()?API?來(lái)評(píng)測(cè)代碼的運(yùn)行速度。該?API?返回當(dāng)前頁(yè)面的高精度時(shí)間戳,您可以在代碼執(zhí)行前后調(diào)用它來(lái)計(jì)算代碼執(zhí)行所需的時(shí)間,這篇文章主要介紹了JavaScript?評(píng)測(cè)代碼運(yùn)行速度,需要的朋友可以參考下2023-02-02
JS中call(),apply(),bind()函數(shù)的區(qū)別與用法詳解
這篇文章主要介紹了JS中call(),apply(),bind()函數(shù)的高級(jí)用法詳解,需要的朋友可以參考下2022-12-12
基于JS實(shí)現(xiàn)的隨機(jī)數(shù)字抽簽實(shí)例
本文分享了基于JS實(shí)現(xiàn)的隨機(jī)數(shù)字抽簽的實(shí)例代碼。小編認(rèn)為具很好的參考價(jià)值,感興趣的朋友可以看下2016-12-12

