Generator 是 ES6 添加的一個特性,容許函數的暫停和恢復,本文使用 generator 構建了一個惰性隊列,並分析其原理。javascript
編寫正確運行的軟件多是困難的,可是咱們知道這僅僅是挑戰的開始。對一個好的解決方案建模能夠把編程從 「能夠運行」 轉換到 「這樣作更好」。相對的,有些事就像下面的註釋同樣:前端
// //親愛的代碼維護者: // //一旦你試圖優化代碼, //而且最終意識到這是一個錯誤的決定, //請把下面的變量加一, //來警告下一個傢伙 // //total_hours_wasted_here = 42 //
(源自: http://stackoverflow.com/ques...java
今天,咱們會去探索 ES6 Generators 的工做原理,從而更好的理解它並用新的方式來解決一些老的問題。node
你可能已經聽過編寫非阻塞 javascript 代碼的重要性。當咱們處理 I/O 操做,好比發送 HTTP 請求或者寫數據庫,咱們一般都會使用回調或者 promises。阻塞代碼會凍結整個應用,在絕大多數場景下都不是一個能夠被使用的方案。webpack
這樣作的另一個後果是,若是你寫了一段無限循環的 javascript 代碼,好比git
node -e 'while(true) {}'
它將極可能凍結你的電腦而且須要系統重啓,請不要在家嘗試。github
考慮到這些,當聽到 ES6 Generators 容許咱們在函數的中間暫停執行而後在將來的某個時候恢復執行時,感到很是好奇。
雖然有些工具好比 Regenerator 和 Babel 已經把這些特性部署到了 ES5。你是否困惑過它們是怎樣作到這些的?今天,咱們會找到真相。
但願咱們能夠更深刻的理解 generators, 更好的發揮它的做用。web
讓咱們從一個簡單的例子開始。好比你要操做一個序列,你可能會建立一個數組而且按照數組的方式操做其值。可是若是這個序列是無限長的呢?數組就不行了,咱們可使用 generator 函數來作:數據庫
function* generateRandoms (max) { max = max || 1; while (true) { let newMax = yield Math.random() * max; if (newMax !== undefined) { max = newMax; } } }
注意 function* 部分,它標示這是一個 「generator 函數」,而且表現與普通函數不一樣。另外一個重要的部分是 yield 關鍵字。普通的函數僅僅經過 return 返回結果,而 generator 函數在 yield 時返回結果。npm
咱們能夠讀出上面函數的意圖 「每次你請求下一個值,它都會給你一個從 0 到 max 的值,直到程序退出(直到人類科技毀滅)。」
根據上面的解讀,咱們僅僅在須要時纔會獲得一個值,這是很是重要的,不然,無限序列會很快的耗盡咱們的內存。咱們使用迭代器來獲取須要的值:
var iterator = generateRandoms(); console.log(iterator.next()); // { value: 0.4900301224552095, done: false } console.log(iterator.next()); // { value: 0.8244022422935814, done: false }
Generators 容許兩種交互,正如咱們下面將要看到的,generators 在沒有被調用時會被掛起,而當迭代器請求下一個值時會被喚醒。因此當咱們屌用 iterator.next 而且傳遞了參數後,參數會被賦值到 newMax:
console.log(iterator.next()); // { value: 0.4900301224552095, done: false } // 爲 `newMax` 賦值,該值會一直存在 console.log(iterator.next(1000)); // { value: 963.7744706124067, done: false } console.log(iterator.next()); // { value: 714.516609441489, done: false }
爲了更好的理解 generators 工做原理,咱們能夠看一下 generators 是怎樣轉換成 ES5 代碼的。你能夠安裝 babel 而後看一下它轉換後的代碼:
npm install -g babel babel generate-randoms.js
下面是轉換後的代碼:
var generateRandoms = regeneratorRuntime.mark(function generateRandoms(max) { var newMax; return regeneratorRuntime.wrap(function generateRandoms$(context$1$0) { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: max = max || 1; case 1: if (!true) { context$1$0.next = 8; break; } context$1$0.next = 4; return Math.random() * max; case 4: newMax = context$1$0.sent; if (newMax !== undefined) { max = newMax; } context$1$0.next = 1; break; case 8: case "end": return context$1$0.stop(); } }, generateRandoms, this); });
如你所見,generator 函數的核心代碼被轉換成了 switch 塊, 這對於咱們探索其內部原理提供了頗有價值的線索。咱們能夠把 generator 想象成一個循環狀態機,它根據咱們的交互切換不一樣的狀態。變量 context$1$0
保存了當前的狀態,case 語句都在該狀態中之行。
看一下這些 switch 塊的條件:
case 0:
初始化 max 的值而且執行到了 case1
。
case 1:
返回一個隨機值而後 GOTO 4
case 4:
檢查迭代器是否設置了 newMax 的值,若是是,就更新 max 的值,而後 GOTO 1
, 返回一個隨機值。
這就解釋了爲何 generator 能夠在遵循非阻塞的原則上能夠無限循環和暫停。
讀者可能注意到我跳過了 9-12 行的代碼:
if (!true) { context$1$0.next = 8; break; }
這裏發生了什麼?它實際上是原始代碼 while (true)
被轉換後的代碼。每當狀態機循環時,它都會檢查是否已經到了最後一步。在咱們的示例中,是沒有循環結束的,但你在編碼時可能會遇到不少時候須要退出循環。當符合循環結束條件時,狀態機 GOTO 8
, generator 之行完畢。
另一個有趣的事情是 generator 是如何爲每個獨立的迭代器保存私有狀態的。由於變量 max 在 regeneratorRuntime.wrap
的外層做用域,它的值會被保留以供以後的 iterator.next()
訪問。
若是咱們調用 randomNumbers()
建立一個新的迭代器,那麼一個新的閉包也會被建立。這也就解釋了迭代器在使用同一個 generator 時有本身的私有狀態而不會相互影響。
目前爲止,咱們已經看到 switch 的本質就是狀態機。你可能已經注意到這個函數被包了兩層:regeneratorRuntime.mark
,regeneratorRuntime.wrap
。
這些是 regenerator 模塊,它能夠在 ES5 中定義 ES6 generator 形式的狀態機。
Regenerator runtime 是一個很長的話題,可是咱們會覆蓋一些有趣的部分。首先,咱們能夠看到 generator 從 「Suspended Start」 狀態開始:
function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) {
源代碼: runtime.js:130,133
在這裏並無發生什麼事,它僅僅是建立並返回了一個函數。這也意味着當咱們調用 var iterator = generateRandoms()
, generateRandoms 內部並無執行。
當咱們調用 iterator.next()
, generator 函數(以前switch 塊中的內容)會在 tryCatch 中被調用:
var record = tryCatch(innerFn, self, context);
源代碼: runtime.js:234
若是返回結果是普通的 return (而不是 throw), 會把結果包裝成 {value, done}
。新的狀態是 GenStateCompleted
或者 GenStateSuspendedYield
。因爲咱們的示例是無限循環,因此將老是跳轉到掛起狀態。
var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; var info = { value: record.arg, done: context.done };
源代碼: runtime.js:234,245
今天咱們用 generator 函數實現了一個惰性序列狀態機。這個特性如今就可使用: 現代瀏覽器都已經原生支持了 generator, 而對於老的瀏覽器,也很容易作代碼轉換。
一般,作一件事情的方式有多種。從這種層面上講,你可能不須要 generators,但若是它容許咱們以一種更富表現力的方式完成目標,那就是值得的。
做者信息
原文做者: Josh Johnston
翻譯自MaxLeap團隊_前端研發人員:Henry Bai
譯者簡介:多年後端及前端開發經驗,現任MaxLeap UX團隊成員,主要從事MaxLeap相關開發,目前對React Native有濃厚興趣。
中文連接:https://blog.maxleap.cn/archi...
相關文章
webpack 入門
做者往期佳做
Redux 最佳實踐「譯」
歡迎訂閱微信公衆號:MaxLeap_yidongyanfa
關於MaxLeap
MaxLeap 移動業務研發的雲服務平臺,爲企業提供包括應用開發所需的後端雲數據庫、雲數據源、雲代碼、雲容器、 IM、移動支付、應用內社交、第三方登陸、社交分享、數據分析、推送營銷,用戶支持等服務, MaxLeap 致力於讓移動應用開發更快速簡單。
官網:https://maxleap.cn/