摘要:在語言和語言中,生成器被稱為迭代器,而在語言中稱為枚舉器。生成器切出執(zhí)行對象并帶出,主線程經(jīng)過同步或異步的處理后,通過方法將帶回生成器的執(zhí)行對象中。向當(dāng)前生成器執(zhí)行對象拋出一個錯誤,并終止生成器的運行。
生成器(Generator)可以說是在 ES2015 中最為強悍的一個新特性,因為生成器是涉及到 ECMAScript 引擎運行底層的特性,生成器可以實現(xiàn)一些從前無法想象的事情。
來龍生成器第一次出現(xiàn)在 CLU1 語言中,這門語言是由 MIT (美國麻省理工大學(xué))的 Barbara Liskov 教授和她的學(xué)生們在 1974 年至 1975 年所設(shè)計和開發(fā)出來的。這門語言雖然古老,但是卻提出了很多如今被廣泛使用的編程語言特性,而生成器便是其中的一個。
而在 CLU 語言之后,有 Icon 語言2、Python 語言3、C# 語言4和 Ruby 語言5等都受 CLU 語言影響,實現(xiàn)了生成器的特性。在 CLU 語言和 C# 語言中,生成器被稱為迭代器(Iterator),而在 Ruby 語言中稱為枚舉器(Enumerator)。
然而無論它被成為什么,所被賦予的能力都是相同的。生成器的主要目的是用于通過一段程序,來持續(xù)被迭代或枚舉出符合某個公式或算法的有序數(shù)列中的元素,而這個程序便是用于實現(xiàn)這個公式或算法,而不需要將目標(biāo)數(shù)列完整寫出。
我們來舉一個簡單的例子,斐波那契數(shù)列是非常著名一個理論數(shù)學(xué)基礎(chǔ)數(shù)列。它的前兩項是 0 和 1,從第三項開始所有的元素都遵循這樣的一條公式:
那么,依靠程序我們可以這樣實現(xiàn):
const fibonacci = [ 0, 1 ] const n = 10 for (let i = 2; i < n - 1; ++i) { fibonacci.push(fibonacci[i - 1] + fibonacci[i - 2]) } console.log(fibonacci) //=> [0, 1, 1, 2, 3, 5, 8, 13, 21]
但是這種需要確定一個數(shù)量來取得相應(yīng)的數(shù)列,但若需要按需獲取元素,那就可以使用生成器來實現(xiàn)了。
function* fibo() { let a = 0 let b = 1 yield a yield b while (true) { let next = a + b a = b b = next yield next } } let generator = fibo() for (var i = 0; i < 10; i++) console.log(generator.next().value) //=> 0 1 1 2 3 5 8 13 21 34 55
你一定會對這段代碼感到很奇怪:為什么 function 語句后會有一個 *?為什么函數(shù)里使用了 while (true) 卻沒有因為進入死循環(huán)而導(dǎo)致程序卡死?而這個 yield 又是什么語句?k4
不必著急,我們一一道來。
基本概念生成器是 ES2015 中同時包含語法和底層支持的一個新特性,其中有幾個相關(guān)的概念是需要先了解的。
生成器函數(shù)(Generator Function)生成器函數(shù)是 ES2015 中生成器的最主要表現(xiàn)方式,它與普通的函數(shù)語法差別在于,在 function 語句之后和函數(shù)名之前,有一個 * 作為它是一個生成器函數(shù)的標(biāo)示符。
function* fibo() { // ... }
生成器函數(shù)的定義并不是強制性使用聲明式的,與普通函數(shù)一樣可以使用定義式進行定義。
const fnName = function*() { /* ... */ }
生成器函數(shù)的函數(shù)體內(nèi)容將會是所生成的生成器的執(zhí)行內(nèi)容,在這些內(nèi)容之中,yield 語句的引入使得生成器函數(shù)與普通函數(shù)有了區(qū)別。yield 語句的作用與 return 語句有些相似,但 yield 語句的作用并非退出函數(shù)體,而是切出當(dāng)前函數(shù)的運行時(此處為一個類協(xié)程,Semi-coroutine),并與此同時可以講一個值(可以是任何類型)帶到主線程中。
我們以一個比較形象的例子來做比喻,你可以把整個生成器運行時看成一條長長的瑞士卷(while (true) 則就是無限長的),ECMAScript 引擎在每一次遇到 yield 就要切一刀,而切面所成的“紋路”則是 yield 出來的值。
生成器(Generator)從計算機科學(xué)角度上看,生成器是一種類協(xié)程或半?yún)f(xié)程(Semi-coroutine),生成器提供了一種可以通過特定語句或方法來使生成器的執(zhí)行對象(Execution)暫停,而這語句一般都是 yield。上面的斐波那契數(shù)列的生成器便是通過 yield 語句將每一次的公式計算結(jié)果切出執(zhí)行對象,并帶到主線程上來。
在 ES2015 中,yield 可以將一個值帶出協(xié)程,而主線程也可以通過生成器對象的方法將一個值帶回生成器的執(zhí)行對象中去。
const inputValue = yield outputValue
生成器切出執(zhí)行對象并帶出 outputValue,主線程經(jīng)過同步或異步的處理后,通過 .next(val) 方法將 inputValue 帶回生成器的執(zhí)行對象中。
使用方法在了解了生成器的背景知識后,我們就可以開始來看看在 ES2015 中,我們要如何使用這個新特性。
構(gòu)建生成器函數(shù)使用生成器的第一步自然是要構(gòu)建一個生成器函數(shù),以生成相對應(yīng)的生成器對象。假設(shè)我們需要按照下面這個公式來生成一個數(shù)列,并以生成器作為構(gòu)建基礎(chǔ)。(此處我們暫不作公式化簡)
為了使得生成器能夠不斷根據(jù)公式輸出數(shù)列元素,我們與上面的斐波那契數(shù)列實例一樣,使用 while (true) 循環(huán)以保持程序的不斷執(zhí)行。
function* genFn() { let a = 2 yield a while (true) { yield a = a / (2 * a + 1) } }
在定義首項為 2 之后,首先將首項通過 yield 作為第一個值切出,其后通過循環(huán)和公式將每一項輸出。
啟動生成器生成器函數(shù)不能直接作為函數(shù)來使用,執(zhí)行生成器函數(shù)會返回一個生成器對象,將用于運行生成器內(nèi)容和接受其中的值。
const gen = genFn()
生成器是是通過生成器函數(shù)的一個生成器(類)實例,我們可以簡單地用一段偽代碼來說明生成器這個類的基本內(nèi)容和用法。
class Generator { next(value) throw(error) [@iterator]() }
操作方法(語法) | 方法內(nèi)容 |
---|---|
generator.next(value) | 獲取下一個生成器切出狀態(tài)。(第一次執(zhí)行時為第一個切出狀態(tài))。 |
generator.throw(error) | 向當(dāng)前生成器執(zhí)行對象拋出一個錯誤,并終止生成器的運行。 |
generator[@iterator] | @iterator 即 Symbol.iterator,為生成器提供實現(xiàn)可迭代對象的方法。使其可以直接被 for...of 循環(huán)語句直接使用。 |
其中 .next(value) 方法會返回一個狀態(tài)對象,其中包含當(dāng)前生成器的運行狀態(tài)和所返回的值。
{ value: Any, done: Boolean }
生成器執(zhí)行對象會不斷檢查生成器的狀態(tài),一旦遇到生成器內(nèi)的最后一個 yield 語句或第一個 return 語句時,生成器便進入終止?fàn)顟B(tài),即狀態(tài)對象中的 done 屬性會從 false 變?yōu)?true。
而 .throw(error) 方法會提前讓生成器進入終止?fàn)顟B(tài),并將 error 作為錯誤拋出。
運行生成器內(nèi)容因為生成器對象自身也是一種可迭代對象,所以我們直接使用 for...of 循環(huán)將其中輸出的值打印出來。
for (const a of gen) { if (a < 1/100) break console.log(a) } //=> // 2 // 0.4 // 0.2222222222 // ...深入理解 運行模式
為了能更好地理解生成器內(nèi)部的運行模式,我們將上面的這個例子以流程圖的形式展示出來。
生成器是一種可以被暫停的運行時,在這個例子中,每一次 yield 都會將當(dāng)前生成器執(zhí)行對象暫停并輸出一個值到主線程。而這在生成器內(nèi)部的代碼是不需要做過多體現(xiàn)的,只需要清楚 yield 語句是暫停的標(biāo)志及其作用即可。
生成器函數(shù)以及生成器對象的檢測事實上 ES2015 的生成器函數(shù)也是一種構(gòu)造函數(shù)或類,開發(fā)者定義的每一個生成器函數(shù)都可以看做對應(yīng)生成器的類,而所產(chǎn)生的生成器都是這些類的派生實例。
在很多基于類(或原型)的庫中,我們可以經(jīng)常看到這樣的代碼。
function Point(x, y) { if (!(this instanceof Point)) return new Point(x, y) // ... } const p1 = new Point(1, 2) const p2 = Point(2, 3)
這一句代碼的作用是為了避免開發(fā)者在創(chuàng)建某一個類的實例時,沒有使用 new 語句而導(dǎo)致的錯誤。而 ECMAScript 內(nèi)部中的絕大部分類型構(gòu)造函數(shù)(不包括 Map 和 Set 及他們的 Weak 版本)都帶有這種特性。
String() //=> "" Number() //=> 0 Boolean() //=> false Object() //=> Object {} Array() //=> [] Date() //=> the current time RegExp() //=> /(?:)/
TIPS: 在代碼風(fēng)格檢查工具 ESLint 中有一個可選特性名為 no-new 即相比使用 new,更傾向于使用直接調(diào)用構(gòu)造函數(shù)來創(chuàng)建實例。
那么同樣的,生成器函數(shù)也支持這種特性,而在互聯(lián)網(wǎng)上的大多數(shù)文獻都使用了直接執(zhí)行的方法創(chuàng)建生成器實例。如果我們嘗試嗅探生成器函數(shù)和生成器實例的原型,我們可以到這樣的信息。
function* genFn() {} const gen = genFn() console.log(genFn.constructor.prototype) //=> GeneratorFunction console.log(gen.constructor.prototype) //=> Generator
這樣我們便可知,我們可以通過使用 instanceof 語句來得知一個生成器實例是否為一個生成器函數(shù)所對應(yīng)的實例。
console.log(gen instanceof genFn) //=> true
十分可惜的是,目前原生支持生成器的主流 JavaScript 引擎(如 Google V8、Mozilla SpiderMonkey)并沒有將 GeneratorFunction 和 Generator 類暴露出來。這就意味著沒辦法簡單地使用 instanceof 來判定一個對象是否是生成器函數(shù)或生成器實例。但如果你確實希望對一個未知的對象檢測它是否是一個生成器函數(shù)或者生成器實例,也可以通過一些取巧的辦法來實現(xiàn)。
對于原生支持生成器的運行環(huán)境來說,生成器函數(shù)自身帶有一個 constructor 屬性指向并沒有被暴露出來的 GeneratorFunction。那么我們就可以利用一個我們已知的生成器函數(shù)的 constructor 來檢驗一個函數(shù)是否是生成器函數(shù)。
function isGeneratorFunction(fn) { const genFn = (function*(){}).constructor return fn instanceof genFn } function* genFn() { let a = 2 yield a while (true) { yield a = a / (2 * a + 1) } } console.log(isGeneratorFunction(genFn)) //=> true
顯然出于性能考慮,我們可以將這個判定函數(shù)利用惰性加載進行優(yōu)化。
function isGeneratorFunction(fn) { const genFn = (function*(){}).constructor return (isGeneratorFunction = fn => fn instanceof genFn)(fn) }
相對于生成器函數(shù),生成器實例的檢測就更為困難。因為無法通過對已知生成器實例自身的屬性來獲取被運行引擎所隱藏起來的 Generator 構(gòu)造函數(shù),所以無法直接用 instanceof 語句來進行類型檢測。也就是說我們需要利用別的方法來實現(xiàn)這個需求。
在上一個章節(jié)中,我們介紹到了在 ECMAScript 中,每一個對象都會有一個 toString() 方法的實現(xiàn)以及其中一部分有 Symbol.toStringTag 作為屬性鍵的屬性,以用于輸出一個為了填補引用對象無法被直接序列化的字符串。而這個字符串是可以間接地探測出這個對象的構(gòu)造函數(shù)名稱,即帶有直接關(guān)系的類。
那么對于生成器對象來說,與它擁有直接關(guān)系的類除了其對應(yīng)的生成器函數(shù)以外,便是被隱藏起來的 Generator 類了。而生成器對象的 @@toStringTag 屬性正正也是 Generator,這樣的話我們就有了實現(xiàn)的思路了。在著名的 JavaScript 工具類庫 LoDash6 的類型檢測中,正式使用了(包括但不限于)這種方法來對未知對象進行類型檢查,而我們也可以試著使用這種手段。
function isGenerator(obj) { return obj.toString ? obj.toString() === "[object Generator]" : false } function* genFn() {} const gen = genFn() console.log(isGenerator(gen)) //=> true console.log(isGenerator({})) //=> false
而另外一方面,我們既然已經(jīng)知道了生成器實例必定帶有 @@toStringTag 屬性并其值夜必定為 Generator,我們也可以通過這個來檢測位置對象是否為生成器實例。
function isGenerator(obj) { return obj[Symbol && Symbol.toStringTag ? Symbol.toStringTag : false] === "Generator" } console.log(isGenerator(gen)) //=> true console.log(isGenerator({})) //=> false
此處為了防止因為運行環(huán)境不支持 Symbol 或 @@toStringTag 而導(dǎo)致報錯,需要使用先做兼容性檢測以完成兼容降級。
而我們再回過頭來看看生成器函數(shù),我們是否也可以使用 @@toStringTag 屬性來對生成器函數(shù)進行類型檢測呢?我們在一個同時支持生成器和 @@toStringTag 的運行環(huán)境中運行下面這段代碼。
function* genFn() {} console.log(genFn[Symbol.toStringTag]) //=> GeneratorFunction
這顯然是可行的,那么我們就來為前面的 isGeneratorFunction 方法再進行優(yōu)化。
function isGeneratorFunction(fn) { return fn[Symbol && Symbol.toStringTag ? Symbol.toStringTag : false] === "GeneratorFunction" } console.log(isGeneratorFunction(genFn)) //=> true
而當(dāng)運行環(huán)境不支持 @@toStringTag 時也可以通過 instanceof 語句來進行檢測。
function isGeneratorFunction(fn) { // If the current engine supports Symbol and @@toStringTag if (Symbol && Symbol.toStringTag) { return (isGeneratorFunction = fn => fn[Symbol.toStringTag] === "GeneratorFunction")(fn) } // Using instanceof statement for detecting const genFn = (function*(){}).constructor return (isGeneratorFunction = fn => fn instanceof genFn)(fn) } console.log(isGeneratorFunction(genFn)) //=> true生成器嵌套
雖然說到現(xiàn)在為止,我們所舉出的生成器例子都是單一生成器進行使用。但是在實際開發(fā)中,我們同樣會遇到需要一個生成器嵌套在另一個生成器內(nèi)的情況,就比如數(shù)學(xué)中的分段函數(shù)或嵌套的數(shù)組公式等。
我們假設(shè)有這樣的一個分段函數(shù),我們需要對其進行積分計算。
分別對分段函數(shù)的各分段作積分,以便編寫程序進行積分。
此處我們可以分別對分段函數(shù)的兩個部分分別建立生成器函數(shù)并使用牛頓-科特斯公式(Newton-Cotes formulas)7來進行積分計算。
// Newton-Cotes formulas function* newton_cotes(f, a, b, n) { const gaps = (b - a) / n const h = gaps / 2 for (var i = 0; i < n; i++) { yield h / 45 * (7 * f(a + i * gaps) + 32 * f(a + i * gaps + 0.25 * gaps) + 12 * f(a + i * gaps + 0.5 * gaps) + 32 * f(a + i * gaps + 0.75 * gaps) + 7 * f(a + (i + 1) * gaps)) } }
在編寫兩個分段部分的生成器之前,我們需要先引入一個新語法 yield*。它與 yield 的區(qū)別在于,yield* 的功能是為了將一個生成器對象嵌套于另一個生成器內(nèi),并將其展開。我們以一個簡單地例子說明。
function* foo() { yield 1 yield 2 } function* bar() { yield* foo() yield 3 yield 4 } for (const n of bar()) console.log(n) //=> // 1 // 2 // 3 // 4
利用 yield* 語句我們就可以將生成器進行嵌套和組合,使得不同的生成器所輸出的值可以被同一個生成器連續(xù)輸出。
function* Part1(n) { yield* newton_cotes(x => Math.pow(x, 2), -2, 0, n) } function* Part2(n) { yield* newton_cotes(x => Math.sin(x), 0, 2, n) } function* sum() { const n = 100 yield* Part1(n) yield* Part2(n) }
最終我們將 sum() 生成器的所有輸出值相加即可。
生成器 ≈ 協(xié)程?從運行機制的角度上看,生成器擁有暫停運行時的能力,那么生成器的運用是否只僅限于生成數(shù)據(jù)呢?在上文中,我們提到了生成器是一種類協(xié)程,而協(xié)程自身是可以通過生成器的特性來進行模擬呢。
在現(xiàn)代 JavaScript 應(yīng)用開發(fā)中,我們經(jīng)常會使用到異步操作(如在 Node.js 開發(fā)中絕大部分使用到的 IO 操作都是異步的)。但是當(dāng)異步操作的層級過深時,就可能會出現(xiàn)回調(diào)地獄(Callback Hell)。
io1((err, res1) => { io2(res1, (err, res2) => { io3(res2, (err, res3) => { io4(res3, (err, res4) => { io5(res5, (err, res5) => { // ...... }) }) }) }) })
顯然這樣很不適合真正的復(fù)雜開發(fā)場景,而我們究竟要如何對著進行優(yōu)化呢?我們知道 yield 語句可以將一個值帶出生成器執(zhí)行環(huán)境,而這個值可以是任何類型的值,這就意味著我們可以利用這一特性做一些更有意思的事情了。
我們回過頭來看看生成器對象的操作方法,生成器執(zhí)行對象的暫停狀態(tài)可以用 .next(value) 方法恢復(fù),而這個方法是可以被異步執(zhí)行的。這就說明如果我們將異步 IO 的操作通過 yield 語句來從生成器執(zhí)行對象帶到主線程中,在主線程中完成后再通過 .next(value) 方法將執(zhí)行結(jié)果帶回到生成器執(zhí)行對象中,這一流程在生成器的代碼中是可以以同步的寫法完成的。
具體思路成型后,我們先以一個簡單的例子來實現(xiàn)。為了實現(xiàn)以生成器作為邏輯執(zhí)行主體,把異步方法帶到主線程去,就要先將異步函數(shù)做一層包裝,使得其可以在帶出生成器執(zhí)行對象之后再執(zhí)行。
// Before function echo(content, callback) { callback(null, content) } // After function echo(content) { return callback => { callback(null, content) } }
這樣我們就可以在生成器內(nèi)使用這個異步方法了。但是還不足夠,將方法帶出生成器執(zhí)行對象后,還需要在主線程將帶出的函數(shù)執(zhí)行才可實現(xiàn)應(yīng)有的需求。上面我們通過封裝所得到的異步方法在生成器內(nèi)部執(zhí)行后,可以通過 yield 語句將內(nèi)層的函數(shù)帶到主線程中。這樣我們就可以在主線程中執(zhí)行這個函數(shù)并得到返回值,然后將其返回到生成器執(zhí)行對象中。
function run(genFn) { const gen = genFn() const next = value => { const ret = gen.next(value) if (ret.done) return ret.value((err, val) => { if (err) return console.error(err) // Looop next(val) }) } // First call next() }
通過這個運行工具,我們便可以將生成器函數(shù)作為邏輯的運行載體,從而將之前多層嵌套的異步操作全部扁平化。
run(function*() { const msg1 = yield echo("Hello") const msg2 = yield echo(`${msg1} World`) console.log(msg2) //=> Hello Wolrd })
通過簡單地封裝,我們已經(jīng)嘗到了一些甜頭,那么再進一步增強之后又會有什么有趣的東西呢?Node.js 社區(qū)中有一個第三方庫名為 co,意為 coroutine,這個庫的意義在于利用生成器來模擬協(xié)程。而我們這里介紹的就是其中的一部分,co 的功能則更為豐富,可以直接使用 Promise 封裝工具,如果異步方法有自帶 Promise 的接口,就無需再次封裝。此外 co 還可以直接實現(xiàn)生成器的嵌套調(diào)用,也就是說可以通過 co 來實現(xiàn)邏輯代碼的全部同步化開發(fā)。
import co from "co" import { promisify } from "bluebird" import fs from "fs" import path from "path" const filepath = path.resolve(process.cwd(), "./data.txt") const defaultData = new Buffer("Hello World") co(function*() { const exists = yield promisify(fs.exists(filepath)) if (exists) { const data = yield promisify(fs.readFile(filepath)) // ... } else { yield promisify(fs.writeFile(filepath, defaultData)) // ... } })Reference
[1] CLU Language http://www.pmg.lcs.mit.edu/CLU.html
[2] Icon Language http://www.cs.arizona.edu/icon
[3] Python Language http://www.python.org
[4] C# Language http://msdn.microsoft.com/pt-br/vcsharp/default.aspx
[5] Ruby Language http://www.ruby-lang.org
[6] LoDash https://lodash.com
[7] Newton-Cotes formulas https://en.wikipedia.org/wiki/Newton%E2%80%93Cotes_formulas
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/79259.html
摘要:不需要多線程的鎖機制線程由系統(tǒng)控制切換,協(xié)程是由用戶控制切換。協(xié)程的中斷實際上是掛起的概念協(xié)程發(fā)起異步操作意味著該協(xié)程將會被掛起,為了保證喚醒時能正常運行,需要正確保存并恢復(fù)其運行時的上下文。 博客 github 地址: https://github.com/HCThink/h-blog/blob/master/js/syncAndAsync/generator/readme.md ...
摘要:說到中的生成器,有人可能會想到協(xié)程,這里我們先不說如何實現(xiàn)協(xié)程,我們探究下的執(zhí)行過程。如果函數(shù)包含了關(guān)鍵字的,那么函數(shù)執(zhí)行后的返回值永遠都是一個對象。如果函數(shù)內(nèi)部同事包含和該函數(shù)的返回值依然是對象,但是在生成對象時,語句后的代碼被忽略。 說到php中的Generator(生成器),有人可能會想到協(xié)程,這里我們先不說php如何實現(xiàn)協(xié)程,我們探究下Generator的執(zhí)行過程。 Gene...
摘要:一的官方資料官方文檔源碼二介紹大致的意思是可以幫助所有版本的和以上版本的生成代碼。其中目前最新的版本可以使用。指定生成一系列對象的環(huán)境。定義了生成的注釋形式。與生成的實體相關(guān)。生成接口和類以達到輕易使用生成的模型和映射文件的目的。 一:MyBatis Generator的官方資料 MyBatis Generator官方文檔github源碼:MyBatis Generator (MBG)...
摘要:關(guān)于協(xié)程和中的什么是協(xié)程進程和線程眾所周知,進程和線程都是一個時間段的描述,是工作時間段的描述,不過是顆粒大小不同,進程是資源分配的最小單位,線程是調(diào)度的最小單位。子程序就是協(xié)程的一種特例。 關(guān)于協(xié)程和 ES6 中的 Generator 什么是協(xié)程? 進程和線程 眾所周知,進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同,進程是 CPU 資源分配的最小單位,...
閱讀 1648·2021-10-12 10:11
閱讀 3764·2021-09-03 10:35
閱讀 1446·2019-08-30 15:55
閱讀 2137·2019-08-30 15:54
閱讀 1004·2019-08-30 13:07
閱讀 1018·2019-08-30 11:09
閱讀 584·2019-08-29 13:21
閱讀 2655·2019-08-29 11:32