Generator Function 是 ES6 提供的一種異步流程控制解決方案。在此以前異步編程形式有,回調函數、事件監聽、發佈/訂閱、Promise 等。但仔細思考前面解決方案,實際仍是以回調函數做爲基礎,並無從語法結構來改變異步寫法。javascript
區別於普通函數,Generator Function 能夠在執行時暫停,後面又能從暫停處繼續執行。一般在異步操做時交出函數執行權,完成後在同位置處恢復執行。新語法更容易在異步場景下達到以同步形式處理異步任務。前端
以前有寫過關於 Promise 解決方式和內部原理實現。接續上文,此篇文章主要闡述 迭代器相關、Generator Function 語法、yield操做符、異步場景使用、經常使用自動執行器、Babel轉譯等。java
注意後文將 Generator Function 翻譯爲生成器函數,個別處簡述生成器。node
在瞭解生成器函數前,有必要先認識下迭代器。迭代器是一種特殊對象,具備專門爲迭代流程設計的 next()
方法。每次調用 next()
都會返回一個包含 value
和 done
屬性的對象。ECMAScript 文檔 The IteratorResult Interface 解釋爲:git
簡單用 ECMAScript 5 語法建立一個符合迭代器接口示例:es6
function createIterator (items) {
var i = 0
return {
next: function () {
var done = (i >= items.length)
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = createIterator([1, 2])
console.log(iterator.next()) // {done: false, value: 1}
console.log(iterator.next()) // {done: false, value: 2}
console.log(iterator.next()) // {done: true, value: undefined}
複製代碼
一般標準的 for 循環代碼,使用變量 i 或 j 等來標示內部索引,每次迭代自增自減維繫正確索引值。對比迭代器,循環語句語法簡單,可是若是要處理多個循環嵌套則須要設置跟蹤多個索引變量,代碼複雜度會大大增長。迭代器的出現必定程度能消除這種複雜性,減小循環中的錯誤。github
除此以外,迭代器提供一致的符合迭代器協議接口,能夠統一可迭代對象遍歷方式。例如 for...of
語句能夠來迭代包含迭代器的可迭代對象(如 Array、Map、Set、String 等)。編程
生成器是一種返回迭代器的函數,經過 function 關鍵字後跟星號 (*) 來表示,此外函數中還須要包含新關鍵字 yield。將上面示例改寫爲生成器函數方式。json
function *createIterator (items) {
for (let i = 0; i < items.length; i++) {
yield items[i]
}
}
const iterator = createIterator([1, 2])
console.log(iterator.next()) // {done: false, value: 1}
console.log(iterator.next()) // {done: false, value: 2}
console.log(iterator.next()) // {done: true, value: undefined}
複製代碼
上述代碼中,經過星號 (*) 代表 createIterator
是一個生成器函數,yield 關鍵字用來指定調用迭代器的 next() 方法時的返回值及返回順序。c#
調用生成器函數並不會當即執行內部語句,而是返回這個生成器的迭代器對象。迭代器首次調用 next() 方法時,其內部會執行到 yield 後的語句爲止。再次調用 next() ,會從當前 yield 以後的語句繼續執行,直到下一個 yield 位置暫停。
next() 返回一個包含 value 和 done 屬性的對象。value 屬性表示本次 yield 表達式返回值,done 表示後續是否還有 yield 語句,即生成器函數是否已經執行完畢。
生成器相關方法以下:
生成器函數繼承於 Function
和 Object
,不一樣於普通函數,生成器函數不能做爲構造函數調用,僅是返回生成器對象。完整的生成器對象關係圖所示:
yield 關鍵字能夠用來暫停和恢復一個生成器函數。yield 後面的表達式的值返回給生成器的調用者,能夠認爲 yield 是基於生成器版本的 return 關鍵字。yield 關鍵字後面能夠跟 任何值 或 表達式。
一旦遇到 yield 表達式,生成器的代碼將被暫停運行,直到生成器的 next() 方法被調用。每次調用生成器的next()方法時,生成器都會在 yield 以後緊接着的語句繼續執行。直到遇到下一個 yield 或 生成器內部拋出異常 或 到達生成器函數結尾 或 到達 return 語句中止。
注意,yield 關鍵字只可在生成器內部使用,在其餘地方使用會致使語法錯誤。即便在生成器內部函數中使用也是如此。
function *createIterator (items) {
items.forEach(item => {
// 語法錯誤
yield item + 1
})
}
複製代碼
另外,yield *
能夠用於聲明委託生成器,即在 Generator 函數內部調用另外一個 Generator 函數。
Generator.prototype.next()
返回一個包含屬性 done 和 value 的對象,也能夠接受一個參數用以向生成器傳值。返回值對象包含的 done 和 value 含義與迭代器章節一致,沒有可過多說道的。值得關注的是,next() 方法能夠接受一個參數,這個參數會替代生成器內部上條 yield 語句的返回值。若是不傳 yield 語句返回值則爲 undefined。例如:
function *createIterator (items) {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let iterator = createIterator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next(4)) // {value: 6, done: false}
console.log(iterator.next()) // {value: NaN, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
複製代碼
有個特例,首次調用 next() 方法時不管傳入什麼參數都會被丟棄。由於傳給 next() 方法的參數會替代上一次 yield 的返回值,而在第一次調用 next() 方法前不會執行任何 yield 語句,因此首次調用時傳參是無心義的。
事實上能給迭代器內部傳值的能力是很重要的。好比在異步流程中,生成器函數執行到 yield 關鍵字處掛起,異步操做完成後須傳遞當前異步值供迭代器後續流程使用。
Generator 函數能夠暫停執行和恢復執行,next() 能夠作函數體內外數據交換,使其能夠做爲異步編程的完整解決方案。以一個異步場景爲例:
function *gen () {
const url = 'https://api.github.com/user/github'
const result = yield fetch(url)
console.log(result.bio)
}
複製代碼
上述代碼中,Generator 函數封裝了一個異步請求操做。除了增長 yield 關鍵字外,上面代碼很是像同步操做。不過運行上述代碼還須要一段執行器代碼。
const g = gen()
const result = g.next()
result.value.then(function (data) => {
g.next(data.json())
})
複製代碼
執行器相關代碼先執行 Generator 函數獲取遍歷器對象,而後使用 next() 執行異步任務的第一階段,在 fetch 返回的 promise.then 方法中調用 next 方法執行第二階段操做。能夠看出,雖然 Generator 函數把異步操做表示得很簡潔,可是流程管理卻不方便,須要額外手動添加運行時代碼。
一般爲了省略額外的手動流程管理,會引入自動執行函數輔助運行。假如生成器函數中 yield 關鍵字後所有爲同步操做,很容易遞歸判斷返回值 done 是否爲 true 運行至函數結束。但更復雜的是異步操做,須要異步完成後執行迭代器 next(data) 方法,傳遞異步結果並恢復接下來的執行。但以何種方式在異步完成時執行 next(),須要提早約定異步操做形式。
經常使用的自動流程管理有 Thunk 函數模式 和 co 模塊。co 一樣能夠支持 Thunk 函數 和 Promise 異步操做。在接下來解釋自動流程管理模塊前,先簡單說道 Thunk 函數。
在 JavaScript 語言中,Thunk 函數指的是將多參數函數替換爲一個只接受回調函數做爲參數的單參數函數(注:這裏多參數函數指的是相似 node 中異步 api 風格,callback 爲最後入參)。相似於函數柯里化的轉換過程,把接受多個參數變換成只接受一個單參數函數。以 node 中異步讀取文件爲例:
// 正常版本的 readFile(多參數)
fs.readFile(fileName, callback)
// Thunk 版本的 readFile (單參數)
const Tunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback)
}
}
const readFileThunk = readFileThunk(fileName)
readFileThunk(callback)
複製代碼
其實任何函數參數中包含回調函數,都能寫成 Thunk 函數形式。相似函數柯里化過程,簡單的 Thunk 函數轉換器以下所示。生成環境建議使用 Thunkify 模塊,能夠處理更多異常邊界狀況。
// Thunk 轉換器
const Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
// 生成 fs.readFile Thunk 函數調用
const readFileThunk = Thunk(fs.readFile)
readFileThunk(fileA)(callback)
複製代碼
先來介紹基於 Thunk 函數的自動流程管理,咱們約定 yield 關鍵字後的表達式返回只接受 callback 參數的函數,即前面講的 Thunk 類型函數。基於 Thunk Generator 簡單自動執行器以下。
function run (fn) {
var gen = fn()
function next (err, data) {
var result = gen.next(data)
if (result.done) return
result.value(next)
}
next()
}
複製代碼
上述自動執行器函數,迭代器首先運行到首個 yield 表達式處,yield 表達式返回只接受參數爲 callback 的函數,同時將 next() 遞歸方法做爲 callback 入參執行。當異步處理完成回掉 callback 時恢復執行生成器函數。
另一種是基於 Promise 對象的自動執行機制。實際上 co 模塊一樣支持,Thunk 函數和 Promise 對象,兩種模式自動流程管理。前者是將異步操做包裝成 Thunk 函數,在 callback 中交回執行權,後者是將異步操做包裝成 Promise 對象,在 then 函數中交回生成器執行權。
沿用上述示例,先將 fs 模塊的 readFile 方法包裝成 Promise 對象。
const fs = require('fs')
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) reject(err)
resolve(data)
})
})
}
複製代碼
相較於 Thunk 模式在 callback 處理遞歸,Promise 對象的自動執行器,則是在 then 方法內調用遞歸處理方法。簡單實現爲:
function fun (gen) {
const g = gen()
function next (data) {
var result = g.next(data)
if (result.done) return result.value
result.value.then((function (data) {
next(data)
}))
}
next()
}
複製代碼
翻閱 co 文檔能夠發現,yield 後對象支持多種形式:promises、thunks、array(promise)、objects (promise)、generators 和 generator functions。大體實現原理與上述一致,這裏就不在貼 co 模塊源碼。更多信息能夠參考 https://github.com/tj/co。
相似於 Promise,其實 Generator 也是有限狀態機,翻閱 ECMAScript 文檔 Properties of Generator Instances 會發現,生成器函數內部存在 undefined
、suspendedStart
、suspendedYield
、executing
、completed
五種狀態。
從語意上很容易理解,伴隨着生成器函數運行,內部狀態發生相應變化。但具體 Generator 內部狀態如何變化,這裏暫時不繼續寫下去,會在下篇文章會結合 Generator es5 運行時源碼講解。
因爲瀏覽器端環境表現不一致,並不能所有原生支持 Generator 函數,通常會採用 babel 插件 facebook/regenerator 進行編譯成 es5 語法,作到低版本瀏覽器兼容。regenerator 提供 transform
和 runtime
包,分別用在 babel 轉碼和 運行時支持。
不一樣於 Promise 對象引入 ployfill 墊片就能夠運行,Generator 函數是新增的語法結構,僅僅依靠添加運行時代碼是沒法在低版本下運行的。Generator 編譯成低版本可用大體流程爲,編譯階段須要處理相應的抽象語法樹(ast),生成符合運行時代碼的 es5 語法結構。運行時階段,添加 runtime 函數輔助編譯後語句執行。
regenerator 網站 提供可視化操做,簡單 ast 轉碼先後示例以下:
function *gen() {
yield 'hello world'
}
var g = gen()
console.log(g.next())
複製代碼
var _marked = regeneratorRuntime.mark(gen);
function gen() {
return regeneratorRuntime.wrap(function gen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 'hello world';
case 2:
case "end":
return _context.stop();
}
}
}, _marked, this);
}
var g = gen();
console.log(g.next());
複製代碼
regenerator-transform
插件處理 ast 語法結構,regenerator-runtime
提供 運行時 regeneratorRuntime 對象支持。這裏涉及到 babel 如何轉碼以及 運行時框架如何運行,內容較多會新起一篇文章再來細說。具體源碼可參考 facebook/regenerator 項目。
插個話題,說到 babel 來科普下常見 bable-polyfill
、babel-runtime
、babel-plugin-transform-runtime
插件功能與區別。babel 只負責 es 語法轉換,不會對新的對象或方法進行轉換,好比 Promise、Array.from 等。babel-polyfill 或 babel-runtime 能夠用來模擬實現相應的對象。
babel-polyfill 和 babel-runtime 二者的區別在於,polyfill 會引入新的全局對象,修改污染掉原有全局做用域下的對象。runtime 則是將開發者依賴的全局內置對象,抽取成單獨的模塊,並經過模塊導入的方式引入,避免對全局做用域污染。
babel-runtime 與 babel-plugin-transform-runtime 區別在於,前者是實際導入項目代碼的功能模塊,後者是用於構建過程的運行時代碼抽取轉換,將所需的運行時代碼引用自 babel-runtime。
能夠參考兩篇文章,babel-polyfill使用與性能優化,babel-runtime使用與性能優化。
阮老師書中有提到相應的關係,能夠在 Generator 函數章節查看。前端不多涉及進程、線程、協程知識點,這裏就不在贅述。
可迭代協議容許 JavaScript 對象去定義它們的迭代行爲, 例如在 for...of 結構中什麼值能夠循環。經常使用數據類型都內置了可迭代對象而且有默認的迭代行爲, 好比 Array、Map, 注意 Object 默認不能使用 for...of 遍歷。
爲了變成可迭代對象,一個對象必須實現 @@iterator
方法, 能夠在這個對象(或者原型鏈上的某個對象)設置 Symbol.iterator 屬性,其屬性值爲返回一個符合迭代器協議對象的無參函數。
接着說迭代器協議,其定義了一種標準的方式來產生序列值。即迭代器對象必須實現 next()
方法且 next()
包含 done 和 value 屬性。兩個屬性同上,前面有過詳細解釋。
簡而言之,可迭代對象必須知足可迭代協議有 Symbol.iterator
方法, Symbol.iterator
方法返回符合迭代器協議對象,包含 next 方法。
看個示例,Object 對象默認不存在迭代器方法,不能使用 for...of 遍歷。咱們能夠修改 Object 原型添加迭代器方法,能夠來訪問相應 key、value 屬性值。
Object.prototype[Symbol.iterator] = function () {
let i = 0
let done, value
const items = Object.entries(this)
return {
next: function () {
done = (i >= items.length)
value = done ? undefined : {
key: items[i][0],
value: items[i][1]
}
i += 1
return {
done: done,
value: value
}
}
}
}
const obj = {
name: 'spurs',
age: '23'
}
for (let item of obj) {
console.log(item)
}
// {key: "name", value: "spurs"}
// {key: "age", value: "23"}
複製代碼
囉哩囉嗦,寫了一篇入門級文章,不少地方都是走馬觀花一句帶過。不過本篇開始也只定位在 Generator 語法入門,後續會再寫篇 Generator 構建與運行時源碼分析。
目前異步流程最佳解決方案已經是 async/await
組合,相比而言語義更清晰,不須要額外自動執行模塊。但其本質上是 Generator 一種語法糖,更好的理解生成器函數會從根源上認識異步流程控制的發展歷程。
最後,若有錯誤,敬請指正。
參考文檔
參考書籍
歡迎關注筆者公衆號