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