摘要:執(zhí)行權由此單向穩(wěn)定的在不同函數(shù)中切換。調用函數(shù)后,引擎會為其開辟一個獨立的函數(shù)執(zhí)行棧以下簡稱棧。執(zhí)行權再次回到外部。成功執(zhí)行完函數(shù),則改變的狀態(tài)為成功。執(zhí)行函數(shù)返回的遍歷器對象會繼承函數(shù)的原型對象。遇到下一個斷點,交出執(zhí)行權傳出返回值。
前言
ES6提供了一種新型的異步編程解決方案:Generator函數(shù)(以下簡稱G函數(shù))。它不是使用JS現(xiàn)有能力按照一定標準制定出來的東西(Promise是如此出生的),而是具有新型底層操作能力,與傳統(tǒng)編程完全不同,代表一種新編程邏輯的高大存在。簡潔方便、受人喜愛的async函數(shù)就是以它為基礎實現(xiàn)的。
1 意義JS引擎是單線程的,只有一個函數(shù)執(zhí)行棧。
當當前函數(shù)執(zhí)行完后,執(zhí)行棧將其彈出,銷毀包含其局部變量的棧空間,并開始執(zhí)行前一個函數(shù)。執(zhí)行權由此單向穩(wěn)定的在不同函數(shù)中切換。雖然Web Worker的出現(xiàn)使我們能夠自行創(chuàng)建多個線程,但這離靈活的控制:暫停執(zhí)行、切換執(zhí)行權和中間的數(shù)據(jù)交換等等,還是很有距離的。
G函數(shù)的意義在于,它可以在單線程的背景下,使執(zhí)行權與數(shù)據(jù)自由的游走于多個執(zhí)行棧之間,實現(xiàn)協(xié)程式編程。
調用G函數(shù)后,引擎會為其開辟一個獨立的函數(shù)執(zhí)行棧(以下簡稱G棧)。在執(zhí)行它的過程中,可以控制暫停執(zhí)行,并將執(zhí)行權轉出給主執(zhí)行棧或另一個G棧(棧在這里可理解為函數(shù))。而此G棧不會被銷毀而是被凍結,當執(zhí)行權再次回來時,會在與上次退出時完全相同的條件下繼續(xù)執(zhí)行。
下面是一個簡單的交出和再次獲得執(zhí)行權的例子。
// 依次打印出:1 2 3 4 5。 let g = G(); console.log("1"); // 執(zhí)行權在外部。 g.next(); // 開始執(zhí)行G函數(shù),遇到 yield 命令后停止執(zhí)行返回執(zhí)行權。 console.log("3"); // 執(zhí)行權再次回到外部。 g.next(); // 再次進入到G函數(shù)中,從上次停止的地方開始執(zhí)行,到最后自動返回執(zhí)行權。 console.log("5"); function* G() { let n = 4; console.log("2"); yield; // 遇到此命令,會暫停執(zhí)行并返回執(zhí)行權。 console.log(n); }2 登堂 2.1 形式
G函數(shù)也是函數(shù),所以具有普通函數(shù)該有的性質,不過形式上有兩點不同。一是在function關鍵字和函數(shù)名之間有一個*號,表示此為G函數(shù)。二是只有在G函數(shù)里才能使用yield命令(以及yield*命令),處于其內部的非G函數(shù)也不行。由于箭頭函數(shù)不能使用yield命令,因此不能用作于Generator函數(shù)(可以用作于async函數(shù))。
以下是它的幾種定義方式。
// 聲明式 function* G() {} // 表達式 let G = function* () {}; // 作為對象屬性 let o = { G: function* () {} }; // 作為對象屬性的簡寫式 let o = { * G() {} }; // 箭頭函數(shù)不能用作G函數(shù),報錯! let o = { G: *() => {} }; // 箭頭函數(shù)可以用作 async 函數(shù)。 let o = { G: async () => {} };2.2 執(zhí)行
調用普通函數(shù)會直接執(zhí)行函數(shù)體中的代碼,之后返回函數(shù)的返回值。但G函數(shù)不同,執(zhí)行它會返回一個遍歷器對象(此對象與數(shù)組中的遍歷器對象相同),不會執(zhí)行函數(shù)體內的代碼。只有當調用它的next方法(也可能是其它實例方法)時,才開始了真正執(zhí)行。
在G函數(shù)的執(zhí)行過程中,碰到yield或return命令時會停止執(zhí)行并將執(zhí)行權返回。當然,執(zhí)行到此函數(shù)末尾時自然會返回執(zhí)行權。每次返回執(zhí)行權之后再次調用它的next方法(也可能是其它實例方法),會重新獲得執(zhí)行權,并從上次停止的地方繼續(xù)執(zhí)行,直到下一個停止點或結束。
// 示例一 let g = G(); g.next(); // 打印出 1 g.next(); // 打印出 2 g.next(); // 打印出 3 function* G() { console.log(1); yield; console.log(2); yield; console.log(3); } // 示例二 let gg = GG(); gg.next(); // 打印出 1 gg.next(); // 打印出 2 gg.next(); // 沒有打印 function* GG() { console.log(1); yield; console.log(2); return; yield; console.log(3); }3 入室 3.1 數(shù)據(jù)交互
數(shù)據(jù)如果不能在執(zhí)行權的更替中取得交互,其存在的意義就會大打折扣。
G函數(shù)的數(shù)據(jù)輸出和輸入是通過yield命令和next方法實現(xiàn)的。
yield和return一樣,后面可以跟上任意數(shù)據(jù),程序執(zhí)行到此會交出控制權并返回其后的跟隨值(沒有則為undefined),作為數(shù)據(jù)的輸出。每次調用next方法將控制權移交給G函數(shù)時,可以傳入任意數(shù)據(jù),該數(shù)據(jù)會等同替換G函數(shù)內部相應的yield xxx表達式,作為數(shù)據(jù)的輸入。
執(zhí)行G函數(shù),返回的是一個遍歷器對象。每次調用它的next方法,會得到一個具有value和done字段的對象。value存儲了移出控制權時輸出的數(shù)據(jù)(即yield或return后的跟隨值),done為布爾值代表該G函數(shù)是否已經(jīng)完成執(zhí)行。作為遍歷器對象的它具有和數(shù)組遍歷器相同的其它性質。
// n1 的 value 為 10,a 和 n2 的 value 為 100。 let g = G(10); let n1 = g.next(); // 得到 n 值。 let n2 = g.next(100); // 相當將 yield n 替換成 100。 function* G(n) { let a = yield n; // let a = 100; console.log(a); // 100 return a; }
實際上,G函數(shù)是實現(xiàn)遍歷器接口最簡單的途徑,不過有兩點需要注意。一是G函數(shù)中的return語句,雖然通過遍歷器對象可以獲得return后面的返回值,但此時done屬性已為true,通過for of循環(huán)是遍歷不到的。二是G函數(shù)可以寫成為永動機的形式,類似服務器監(jiān)聽并執(zhí)行請求,這時通過for of遍歷是沒有盡頭的。
--- 示例一:return 返回值。 let g1 = G(); console.log( g1.next() ); // value: 1, done: false console.log( g1.next() ); // value: 2, done: true console.log( g1.next() ); // value: undefined, done: true let g2 = G(); for (let v of g2) { console.log(v); // 只打印出 1。 } function* G() { yield 1; return 2; } --- 示例二:作為遍歷器接口。 let o = { id: 1, name: 2, ago: 3, *[Symbol.iterator]() { let arr = Object.keys(this); for (let v of arr) { yield this[v]; // 使用 yield 輸出。 } } } for (let v of o) { console.log(v); // 依次打印出:1 2 3。 } --- 示例三:永動機。 let g = G(); g.next(); // 打印出: Do ... 。 g.next(); // 打印出: Do ... 。 // ... 可以無窮次調用。 // 可以嘗試此例子,雖然頁面會崩潰。 // 崩潰之后可以點擊關閉頁面,或終止瀏覽器進程,或辱罵作者。 for (let v of G()) { console.log(v); } function* G() { while (true) { console.log("Do ..."); yield; } }3.2 yield*
yield*命令的基本原理是自動遍歷并用yield命令輸出擁有遍歷器接口的對象,怪繞口的,直接看示例吧。
// G2 與 G22 函數(shù)等價。 for (let v of G1()) { console.log(v); // 打印出:1 [2, 3] 4。 } for (let v of G2()) { console.log(v); // 打印出:1 2 3 4。 } for (let v of G22()) { console.log(v); // 打印出:1 2 3 4。 } function* G1() { yield 1; yield [2, 3]; yield 4; } function* G2() { yield 1; yield* [2, 3]; // 使用 yield* 自動遍歷。 yield 4; } function* G22() { yield 1; for (let v of [2, 3]) { // 等價于 yield* 命令。 yield v; } yield 4; }
在G函數(shù)中直接調用另一個G函數(shù),與在外部調用沒什么區(qū)別,即便前面加上yield命令。但如果使用yield*命令就能直接整合子G函數(shù)到父函數(shù)中,十分方便。因為G函數(shù)返回的就是一個遍歷器對象,而yield*可以自動展開持有遍歷器接口的對象,并用yield輸出。如此就等價于將子G函數(shù)的函數(shù)體原原本本的復制到父G函數(shù)中。
// G1 與 G2 等價。 for (let v of G1()) { console.log(v); // 依次打印出:1 2 "-" 3 4 } for (let v of G2()) { console.log(v); // 依次打印出:1 2 "-" 3 4 } function* G1() { yield 1; yield* GG(); yield 4; } function* G2() { yield 1; yield 2; console.log("-"); yield 3; yield 4; } function* GG() { yield 2; console.log("-"); yield 3; }
唯一需要注意的是子G函數(shù)中的return語句。yield*雖然與for of一樣不會遍歷到該值,但其能直接返回該值。
let g = G(); console.log( g.next().value ); // 1 console.log( g.next().value ); // undefined, 打印出 return 2。 function* G() { let n = yield* GG(); // 第二次執(zhí)行 next 方法時,這里等價于 let n = 2; 。 console.log("return", n); } function* GG() { yield 1; return 2; }3.3 異步應用
歷經(jīng)了如此多的鋪墊,是到將其應用到異步的時候了,來來來,喝了這壇酒咱就到馬路上碰個瓷試試運氣。
使用G函數(shù)處理異步的優(yōu)勢,相對于在這以前最優(yōu)秀的Promise來說,在于形式上使主邏輯代碼更為的精簡和清晰,使其看起來與同步代碼基本相同。雖然在日常生活中,我們說誰誰做事愛搞形式多少包含有貶低意味。但在這程序的世界,對于我們編寫和他人閱讀來說,這些改進的效益可是相當可觀哦。
// 模擬請求數(shù)據(jù)。 // 依次打印出 get api1, Do ..., get api2, Do ..., 最終值:3000 。 // 請求數(shù)據(jù)的主邏輯塊 function* G() { let api1 = yield createPromise(1000); // 發(fā)送第一個數(shù)據(jù)請求,返回的是該 Promise 。 console.log("get api1", api1); // 得到數(shù)據(jù)。 console.log("Do somethings with api1"); // 做些操作。 let api2 = yield createPromise(2000); // 發(fā)送第二個數(shù)據(jù)請求,返回的是該 Promise 。 console.log("get api2", api2); // 得到數(shù)據(jù)。 console.log("Do somethings with api2"); // 做些操作。 return api1 + api2; } // 開始執(zhí)行G函數(shù)。 let g = G(); // 得到第一個 Promise 并等待其返回數(shù)據(jù) g.next().value.then(res => { // 獲取到第一個請求的數(shù)據(jù)。 return g.next(res).value; // 將第一個數(shù)據(jù)傳回,并獲取到第二個 Promise 。 }).then(res => { // 獲取到第二個請求的數(shù)據(jù)。 return g.next(res).value; // 將第二個數(shù)據(jù)傳回。 }).then(res => { console.log("最終值:", res); }); // 模擬請求數(shù)據(jù) function createPromise(time) { return new Promise(resolve => { setTimeout(() => { resolve(time); }, time); }); }
上面的方式有很大的優(yōu)化空間。我們執(zhí)行函數(shù)時的邏輯是:先獲取到異步請求并等待其返回結果,再將結果傳遞回G函數(shù),之后重復操作。而按照此方式,意味著G函數(shù)中有多少異步請求,我們就應該重復多少次該操作。如果觀眾老爺們足夠敏感,此時就能想到這些步奏是能抽象成一個函數(shù)的。而抽象出來的這個函數(shù)就是G函數(shù)的自執(zhí)行器。
以下是一個簡易的自執(zhí)行器,它會返回一個Promise。再往內是通過遞歸一步步的執(zhí)行G函數(shù),對其返回的結果都統(tǒng)一使用resolve方法包裝成Promise對象。
// 與上一個示例等價。 RunG(G).then(res => { console.log("G函數(shù)執(zhí)行結束:", res); // 3000 }); function* G() { let api1 = yield createPromise(1000); console.log("get api1", api1); console.log("Do somethings with api1"); let api2 = yield createPromise(2000); console.log("get api2", api2); console.log("Do somethings with api2"); return api1 + api2; } function RunG(G) { // 返回 Promise 對象。 return new Promise((resolve, reject) => { let g = G(); next(); function next(data) { let r = g.next(data); // 成功執(zhí)行完G函數(shù),則改變 Promise 的狀態(tài)為成功。 if (r.done) return resolve(r.value); // 將每次的返回值統(tǒng)一包裝成 Promise 對象。 // 成功則繼續(xù)執(zhí)行G函數(shù),否則改變 Promise 的狀態(tài)為失敗。 Promise.resolve(r.value).then(next).catch(reject); } }); } function createPromise(time) { return new Promise(resolve => { setTimeout(() => { resolve(time); }, time); }); }
自執(zhí)行器可以自動執(zhí)行任意的G函數(shù),是應用于異步時必要的咖啡伴侶。上面是接地氣的寫法,我們來看看較為官方的版本。可以直觀的感受到,兩者主要的區(qū)別在對可能錯誤的捕獲和處理上,這也是平常寫的代碼和構建底層庫主要的區(qū)別之一。
function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); }4 實例方法
實例方法比如next以及接下來的throw和return,實際是存在G函數(shù)的原型對象中。執(zhí)行G函數(shù)返回的遍歷器對象會繼承G函數(shù)的原型對象。在此添加自定義方法也可以被繼承。這使得G函數(shù)看起來類似構造函數(shù),但實際兩者不相同。因為G函數(shù)本就不是構造函數(shù),不能被new,內部的this也不能被繼承。
function* G() { this.id = 123; } G.prototype.sayName = () => { console.log("Wmaker"); }; let g = G(); g.id; // undefined g.sayName(); // "Wmaker"4.1 throw
實例方法throw和next方法的性質基本相同,區(qū)別在于其是向G函數(shù)體內傳遞錯誤而不是值。通俗的表達是將yield xxx表達式替換成throw 傳入的參數(shù)。其它比如會接著執(zhí)行到下一個斷點,返回一個對象等等,和next方法一致。該方法使得異常處理更為簡單,而且多個yield表達式可以只用一個try catch代碼塊捕獲。
當通過throw方法或G函數(shù)在執(zhí)行中自己拋出錯誤時。如果此代碼正好被try catch塊包裹,便會像公園里行完方便的寵物一樣,沒事的繼續(xù)往下執(zhí)行。遇到下一個斷點,交出執(zhí)行權傳出返回值。如果沒有錯誤捕獲,JS會終止執(zhí)行并認為函數(shù)已經(jīng)結束運行,此后再調用next方法會一直返回value為undefined、done為true的對象。
// 依次打印出:1, Error: 2, 3。 let g = G(); console.log( g.next().value ); // 1 console.log( g.throw(2).value ); // 3,打印出 Error: 2。 function* G() { try { yield 1; } catch(e) { console.log("Error:", e); } yield 3; } // 使用了 throw(2) 等價于使用 next() 并將代碼改寫成如下所示。 function* G() { try { yield 1; throw 2; // 替換原來的 yield 表達式,相當在后面添加。 } catch(e) { console.log("Error:", e); } yield 3; }4.2 return
實例方法return和throw的情況相同,與next具有相似的性質。區(qū)別在于其會直接終止G函數(shù)的執(zhí)行并返回傳入的參數(shù)。通俗的表達是將yield xxx表達式替換成return 傳入的參數(shù)。值得注意的是,如果此時正好處于try代碼塊中,且其帶有finally模塊,那么return方法會推遲到finally代碼塊執(zhí)行完后再執(zhí)行。
let g = G(); console.log( g.next().value ); // 1 console.log( g.return(4).value ); // 2 console.log( g.next().value ); // 3 console.log( g.next().value ); // 4,G函數(shù)結束。 console.log( g.next().value ); // undefined function* G() { try { yield 1; } finally { yield 2; yield 3; } yield 5; } // 使用了 return(4) 等價于使用 next() 并將代碼改寫成如下所示。 function* GG() { try { yield 1; return 4; // 替換原來的 yield 表達式,相當在后面添加。 } finally { yield 2; yield 3; } yield 5; }延伸
ES6精華:Symbol
ES6精華:Promise
Iterator:訪問數(shù)據(jù)集合的統(tǒng)一接口
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108377.html
摘要:關鍵字表示代碼在該處將會被阻塞式暫停阻塞的僅僅是函數(shù)代碼本身,而不是整個程序,但是這并沒有引起函數(shù)內部自頂向下代碼的絲毫改變。通過實現(xiàn)模式在通過實現(xiàn)理論的過程中已經(jīng)有一些有趣的探索了。 至此本系列的四篇文章翻譯完結,查看完整系列請移步blogs 由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,望不吝指正issue ES6 Generators: 完整系列 The Basics...
摘要:如果你還沒讀過上篇上篇和中篇并無依賴關系,您可以讀過本文之后再閱讀上篇,可戳面試篇寒冬求職季之你必須要懂的原生上小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。 互聯(lián)網(wǎng)寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環(huán)境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包,this,原型鏈,就能獲得...
摘要:如果你還沒讀過上篇上篇和中篇并無依賴關系,您可以讀過本文之后再閱讀上篇,可戳面試篇寒冬求職季之你必須要懂的原生上小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關知識點。 互聯(lián)網(wǎng)寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環(huán)境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包,this,原型鏈,就...
摘要:文章同步自個人博客此前只是簡單使用而沒有真正的去研究,這次要好好折騰下這貨。我們要實現(xiàn)一個啟動器來運行它,并把結果傳給下一次,這樣就實現(xiàn)了接收值的功能。就啟動起來了,并且一直執(zhí)行到為為止。如果執(zhí)行不了,請升級瀏覽器,本例在下通過。 文章同步自個人博客:http://www.52cik.com/2016/07/11/generator-co.html 此前只是簡單使用而沒有真正的去研究 ...
摘要:換句話說,我們很好的對代碼的功能關注點進行了分離通過將使用消費值得地方函數(shù)中的邏輯和通過異步流程來獲取值迭代器的方法進行了有效的分離。但是現(xiàn)在我們通過來管理代碼的異步流程部分,我們解決了回調函數(shù)所帶來的反轉控制等問題。 本文翻譯自 Going Async With ES6 Generators 由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,還望指正Issue ES6 Gener...
閱讀 1103·2021-11-16 11:45
閱讀 3123·2021-10-13 09:40
閱讀 714·2019-08-26 13:45
閱讀 1187·2019-08-26 13:32
閱讀 2166·2019-08-26 13:23
閱讀 911·2019-08-26 12:16
閱讀 2823·2019-08-26 11:37
閱讀 1747·2019-08-26 10:32