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