摘要:遍歷器原有的表示集合的數據結構,主要有和,在中又加入了和,這樣就有了四種數據集合,還可以組合使用它們,如數組的成員是或,這樣就需要一種統一的接口機制,用來處理所有不同的數據結構。
閱讀原文
Generator 函數是 ES6 提供的一種異步編程解決方案,是一個生成器,用于生成一個遍歷器的函數,語法行為與傳統函數完全不同。
Iterator 遍歷器JavaScript 原有的表示 “集合” 的數據結構,主要有 Array 和 Object,在 ES6 中又加入了 Set 和 Map,這樣就有了四種數據集合,還可以組合使用它們,如數組的成員是 Map 或 Object,這樣就需要一種統一的接口機制,用來處理所有不同的數據結構。
遍歷器 Iterator 就是這樣一種機制,它是一種接口,為不同的數據結構提供統一的、簡便的訪問機制,任何數據結構只要部署了 Iterator 接口,就可以完成遍歷操作,即依次處理該數據結構的所有成員。
Iterator 遍歷器其實就是一個指針對象,上面有 next 方法,第一次調用 next 指針指向數據結構的第一個成員,第二次 next 調用指針指向第二個成員,直到指針指向最后一個成員。
我們可以使用 ES6 的展開運算符 ... 和 for...of... 去遍歷帶有 Iterator 接口的數據結構,需要注意的是,Object 本身不具備 Iterator 接口,所以我們無法通過 ... 把一個對象擴展到一個數組中,并且會報錯,我們可以通過代碼手動將 Object 類型實現 Iterator 接口。
// 給對象擴展 Iterator 接口 // 通過 Generator 函數給 Object 擴展 Iterator 接口 Object.prototype[Symbol.iterator] = function*() { for (var key in this) { yield this[key]; } }; // 測試 Iterator 接口 let obj = { a: 1, b: 2, c: 3 }; let arr = [...obj]; console.log(arr); // [1, 2, 3]
上面我們其實是通過 ES6 的 Generator 函數簡單粗暴的給 Object 類型實現了 Iterator 接口,后面我們會簡單模擬 Generator 生成器。
模擬 GeneratorGenerator 函數是一個生成器,調用后會返回給我們一個 Iterator 遍歷器對象,在對象中有一個 next 方法,調用一次 next,幫我遍歷一次,返回值為一個對象,內部有 value 和 done 兩個屬性,value 屬性代表當前遍歷的值,done 屬性代表是否遍歷完成,如果遍歷完成后繼續調用 next,返回的對象中 value 屬性值為 undefined,done 屬性值為 true,這個遍歷器在進行數據遍歷時更像給我們提供了一個暫停功能,每次都需要手動調用 next 去進行下一次遍歷。
我們根據 Generator 的特性用 ES5 簡單模擬一個遍歷器生成函數:
// 模擬遍歷器生成函數 function iterator(arr) { var i = 0; return { next: function() { var done = i >= arr.length; var value = !done ? arr[i++] : undefined; return { value: value, done: done }; } }; }
測試一下模擬的遍歷器生成函數:
// 測試 iterator 函數 var arr = [1, 3, 5]; // 遍歷器 var result = iterator(arr); result.next(); // {value: 1, done: false} result.next(); // {value: 3, done: false} result.next(); // {value: 5, done: false} result.next(); // {value: undefined, done: true}Generator 的基本使用
在普通的函數 function 關鍵字后加一個 * 就代表聲明了一個生成器函數,執行后返回一個遍歷器對象,每次調用遍歷器的 next 方法時,遇到 yield 關鍵字暫停執行,并將 yield 關鍵字后面的值會作為返回對象中 value 的值,如果函數有返回值,會把返回值作為調用 next 方法進行遍歷完成后返回的對象中 value 的值,果已經遍歷完成,再次 next 調用這個 value 的值會變成 undefined。
// 生成器函數 function* gen() { yield 1; yield 2; return 3; } // 遍歷器 let it = gen(); it.next(); // {value: 1, done: false} it.next(); // {value: 2, done: false} it.next(); // {value: 3, done: true} it.next(); // {value: undefined, done: true}
在 Generator 函數中可以使用變量接收 yield 關鍵字執行后的返回值,只是接收的值并不是 yield 關鍵字后面表達式執行的結果,而是遍歷器在下一次調用 next 方法時傳入的參數。
也就是說我們第一次調用 next 方法進行遍歷時是不需要傳遞參數的,因為上面并沒有變量來接收它,即使傳參也會被忽略掉,我們用一個例子感受一下這種比較特殊的執行機制:
// 生成器函數 function* gen(arr) { let a = yield 1; let b = yield a; let c = yield b; return c; } // 遍歷器 let it = gen(); it.next(); // {value: 1, done: false} it.next(2); // {value: 2, done: false} it.next(3); // {value: 3, done: false} it.next(4); // {value: 4, done: true} it.next(5); // {value: undefined, done: true}
如果已經遍歷完成,并把上次遍歷接收到的值作為返回值傳遞給返回對象 value 屬性的值,后面再次調用 next 傳入的參數也會被忽略,返回對象的 value 值為 undefined。
在 Generator 函數中,如果在其他函數或方法調用的回調內部(函數的執行上/下文發生變化)不能直接使用 yield 關鍵字。
// 循環中使用 yield // 錯誤的寫法 function* gen(arr) { arr.forEach(*item => { yield* item; }); } // 正確的寫法 function* gen(arr) { for(let i = 0; i < arr.length; i++) { yield arr[i]; } }
如果在一個 Generator 函數中調用了另一個 Generator 函數,在調用外層函數返回遍歷器的 next 方法時是不會遍歷內部函數返回的遍歷器的。
// 合并生成器 —— 錯誤 // 外層的生成器函數 function* genOut() { yield "a"; yield genIn(); yield "c"; } // 內層的生成器函數 function* genIn() { yield "b"; } // 遍歷器 let it = genOut(); it.next(); // {value: "a", done: false} it.next(); // 返回 genIn 的遍歷器對象 it.next(); // {value: "c", done: false} it.next(); // {value: undefined, done: true}
上面代碼如果想在調用 genOut 返遍歷器的 next 方法時,同時遍歷 genIn 調用后返回的遍歷器,需要使用 yield* 表達式。
// 合并生成器 —— yield* // 外層的生成器函數 function* genOut() { yield "a"; yield* genIn(); yield "c"; } // 內層的生成器函數 function* genIn() { yield "b"; } // 遍歷器 let it = genOut(); it.next(); // {value: "a", done: false} it.next(); // {value: "b", done: false} it.next(); // {value: "c", done: false} it.next(); // {value: undefined, done: true}
在 genOut 返回的遍歷器調用 next 遇到 yield* 表達式時幫我們去遍歷了 genIn 返回的遍歷器,其實 yield* 內部做了處理,等同于下面代碼:
// 合并生成器 —— for of // 外層的生成器 function* genOut() { yield "a"; for (let v of genIn()) { yield v; } yield "c"; } // 內層的生成器 function* genIn() { yield "b"; } // 遍歷器 let it = genOut(); it.next(); // {value: "a", done: false} it.next(); // {value: "b", done: false} it.next(); // {value: "c", done: false} it.next(); // {value: undefined, done: true}
Promise 也是 ES6 的規范,同樣是解決異步的一種手段,如果對 Promise 還不了解,可以閱讀下面兩篇文章:
異步發展流程 —— Promise 的基本使用
異步發展流程 —— 手寫一個符合 Promise/A+ 規范的 Promise
因為 Generator 函數在執行時遇到 yield 關鍵字會暫停執行,那么 yield 后面可以是異步操作的代碼,比如 Promise,需要繼續執行,就手動調用返回遍歷器的 next 方法,因為中間有一個等待的過程,所以在執行異步代碼的時候避免了回調函數的嵌套,在寫法上更像同步,更容易理解。
我們來設計一個 Generator 函數與 Promise 異步操作結合的使用場景,假設我們需要使用 NodeJS 的 fs 模塊讀取一個文件 a.txt 的內容,而 a.txt 的內容是另一個需要讀取文件 b.txt 的文件名,讀取 b.txt 最后打印讀取到的內容 “Hello world”。
回調函數的實現:
// 連續讀取文件 —— 異步回調 // 引入依賴 const fs = require("fs"); fs.readFile("a.txt", "utf8", (err, data) => { if (!err) { fs.readFile(data, "utf8", (err, data) => { if (!err) { console.log(data); // Hello world } }); } });
上面代碼因為只有兩層回調函數嵌套,所以感覺沒那么復雜,但是嵌套的回調函數多了,代碼就不那么的優雅了,我們接下來使用 Generator 結合 Promise 來實現,為了方便將 fs 異步的方法轉換成 Promise,我們引入 util 模塊,并轉換 readFile 方法。
// 連續讀取文件 —— Generator + Promise // 引入依賴 const fs = require("fs"); const util = require("util"); // 將 readFile 方法轉換成 Promise const read = util.promisify(fs.readFile); // 生成器函數 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 遍歷器 let it = gen(); it.next().value.then(data => { it.next(data).then(data => { console.log(data); // Hello world }); });
我們只看 Generator 函數 gen 內部的執行,雖然是異步操作,但是在寫法上幾乎和同步沒有區別,理解起來更容易,唯一美中不足的是,我們需要自己手動的調用遍歷器的 next 和 Promise 實例的 then,這個問題 co 庫可以幫我們解決。
co 庫的使用co 庫的作者是著名的 NodeJS 大神 TJ,是基于 Generator 和 Promise 的,這個庫能幫我們實現自動調用 Generator 函數返回遍歷器的 next 方法,并執行 yield 后面 Promise 實例的 then 方法,所以每次 yield 后面的異步操作返回的必須是一個 Promise 實例,代碼看起來像同步,執行其實是異步,不用自己手動進行下一次遍歷,這更是我們想要的。
由于 co 是一個第三方的模塊,所以在使用時需要我們提前下載:
npm install co
我們使用 co 來實現之前異步連續讀文件的案例:
// 連續讀取文件 —— Generator + co // 引入依賴 const fs = require("fs"); const util = require("util"); const co = require("co"); // 將 readFile 方法轉換成 Promise const read = util.promisify(fs.readFile); // 生成器函數 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 使用 co 庫代替手動調用 next co(gen()).then(data => { console.log(data); // Hello world });
從上面代碼可以看出,co 庫的 co 函數參數是一個遍歷器,即 Generator 函數執行后的返回結果,在 co 內部操作遍歷器并遍歷完成后返回了一個 Promise 實例,遍歷器最終的返回結果的 value 值作為 then 方法回調的參數,所以我們可以使用 then 對結果進行后續的處理。
co 庫的實現原理我們其實在上面使用 co 的過程中對于 co 函數的內部做了什么已經有所了解,主要就是幫助我們調用遍歷器的 next 和調用 yield 后面代碼執行后返回 Promise 實例的 then,并在整個遍歷結束后,返回一個新的 Promise 實例。
下面我們根據上面分析的 co 函數的原理來模擬一個簡易版的 co 庫:
// 文件:myCo.js —— co 原理 // co 函數,it 為遍歷器對象 function co(it) { // 返回 Promise 實例 return new Promise((resolve, reject) => { // 異步遞歸 function next(data) { // 第一次調用 next 不需要傳參 let { value, done } = it.next(data); if (!done) { // 如果沒完成遍歷,調用返回 Promise 的 then 方法 value.then(data => { // 如果 Promise 成功,繼續遞歸,如果失敗直接執行 reject next(data); }, reject); } else { // 如果遍歷完成直接執行 resolve 并傳入 value resolve(value); } } next(); }); } // 導出模塊 module.exports = co;
驗證 myCo.js 實現的 co 函數:
// 驗證 myCo // 引入依賴 const fs = require("fs"); const util = require("util"); const myCo = require("./myCo"); // 將 readFile 方法轉換成 Promise const read = util.promisify(fs.readFile); // 生成器函數 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 使用 co 庫代替手動調用 next myCo(gen()).then(data => { console.log(data); // Hello world });
我們將引入的 co 庫替換成了自己實現的簡易版 myCo 模塊,上面讀取文件的案例依然生效,這說明我們模擬的 co 庫核心邏輯是沒問題的,跟原版不同的是并沒有處理很多細節,并定義指針,如果對 co 庫感興趣建議看看 TJ 大神的源碼,整個庫寫的非常精簡,值得學習。
總結Generators 相當于把一個函數拆分成若干個部分執行,執行一次時將指針指向下一段要執行的代碼,直到結束位置,Generators 配合 co 庫的使用場景多在 NodeJS 當中,并在 Koa 1.x 版本中居多,現在已經升級到 Koa 2.x 版本,使用更多的是基于 Generators 和 co 庫衍生出來的 ES7 新標準 async/await,我們在下一篇異步發展流程系列的文章中來詳細介紹。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98280.html
摘要:簡介指的是兩個關鍵字,是引入的新標準,關鍵字用于聲明函數,關鍵字用來等待異步必須是操作,說白了就是的語法糖。最后希望大家在讀過異步發展流程這個系列之后,對異步已經有了較深的認識,并可以在不同情況下游刃有余的使用這些處理異步的編程手段。 showImg(https://segmentfault.com/img/remote/1460000018998406?w=1024&h=379); ...
摘要:以下展示它是如何工作的函數使用構造函數創建一個新的對象,并立即將其返回給調用者。在傳遞給構造函數的函數中,我們確保傳遞給,這是一個特殊的回調函數。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專欄同步: Encounter的掘金專欄 知乎專欄...
摘要:所以僅用于簡化理解,快速入門,依然需要閱讀有深入研究的文章來加深對各種異步流程控制的方法的掌握。 原文地址:http://zodiacg.net/2015/08/javascript-async-control-flow/ 隨著ES6標準逐漸成熟,利用Promise和Generator解決回調地獄問題的話題一直很熱門。但是對解決流程控制/回調地獄問題的各種工具認識仍然比較麻煩。最近兩天...
摘要:注是先前版本處理異步函數的方式,通過可以將異步函數封裝成,傳入普通參數后形成僅需要參數的偏函數,以此簡化調用代碼目前中的偏函數已經被無情地化了。 前幾天研究了TJ的koa/co4.x和一系列koa依賴的源碼,在知乎上做出了人生首次回答(而且我真得再也不想去知乎回答技術問題了_(:з」∠)_),因此把文字搬到這里。 ES2015 Generator/Yield 關于Generator...
摘要:更好的異步編程上面的方法可以適用于那些比較簡單的異步工作流程。小結的組合目前是最強大,也是最優雅的異步流程管理編程方式。 訪問原文地址 generators主要作用就是提供了一種,單線程的,很像同步方法的編程風格,方便你把異步實現的那些細節藏在別處。這讓我們可以用一種很自然的方式書寫我們代碼中的流程和狀態邏輯,不再需要去遵循那些奇怪的異步編程風格。 換句話說,通過將我們generato...
閱讀 1446·2021-11-16 11:44
閱讀 3286·2021-09-29 09:43
閱讀 620·2019-08-30 10:52
閱讀 938·2019-08-29 11:01
閱讀 3258·2019-08-26 11:47
閱讀 2886·2019-08-23 12:18
閱讀 1359·2019-08-22 17:04
閱讀 2046·2019-08-21 17:04