<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在使用 Generator
前,首先知道 Generator
是什麼。
如果讀者有 Python 開發經驗,就會發現,無論是概念還是形式上,ES2015 中的 Generator
幾乎就是 Python 中 Generator
的翻版。
Generator
本質上是一個函數,它最大的特點就是可以被中斷,然後恢復執行。通常來說,當開發者呼叫一個函數之後,這個函數的執行就脫離了開發者的控制,只有函數執行完畢之後,控制權才能重新回到呼叫者手中,因此程式設計師在編寫方法程式碼時,唯一
能夠影響方法執行的只有預先定義的 return
關鍵字。
Promise
也是如此,我們也無法控制 Promise
的執行,新建一個 Promise
後,其狀態自動轉換為 pending
,同時開始執行,直到狀態改變後我們才能進行下一步操作。
而 Generator
函數不同,Generator
函數可以由使用者執行中斷或者恢復執行的操作,Generator
中斷後可以轉去執行別的操作,然後再回過頭從中斷的地方恢復執行。
Generator
函數和普通函數在外表上最大的區別有兩個:
function
關鍵字和方法名中間有個星號(*)。yield
關鍵字。function* Generator() { yield "Hello World"; return "end"; }
和普通方法一樣,Generator
可以定義成多種形式:
// 普通方法形式 function* generator() {} //函數表示式 const gen = function* generator() {} // 物件的屬性方法 const obi = { * generator() { } }
Generator 函數的狀態
yield
關鍵字用來定義函數執行的狀態,在前面程式碼中,如果 Generator
中定義了 x
個 yield
關鍵字,那麼就有 x + 1
種狀態(+1是因為最後的 return
語句)。
跟普通函數相比,Generator
函數更像是一個類或者一種資料型別,以下面的程式碼為例,直接執行一個 Generator
會得到一個 Generator
物件,而不是執行方法體中的內容。
const gen = Generator();
按照通常的思路,gen
應該是 Generator()
函數的返回值,上面也提到Generator
函數可能有多種狀態,讀者可能會因此聯想到 Promise
,一個 Promise
也可能有三種狀態。不同的是 Promise
只能有一個確定的狀態,而 Generator
物件會逐個經歷所有的狀態,直到 Generator
函數執行完畢。
當呼叫 Generator
函數之後,該函數並沒有立刻執行,函數的返回結果也不是字串,而是一個物件,可以將該物件理解為一個指標,指向 Generator
函數當前的狀態。(為了便於說明,我們下面採用指標的說法)。
當 Generator
被呼叫後,指標指向方法體的開始行,當 next
方法呼叫後,該指標向下移動,方法也跟著向下執行,最後會停在第一個遇到的 yield
關鍵字前面,當再次呼叫 next
方法時,指標會繼續移動到下一個 yield
關鍵字,直到執行到方法的最後一行,以下面程式碼為例,完整的執行程式碼如下:
function* Generator() { yield "Hello World"; return "end"; } const gen = Generator(); console.log(gen.next()); // { value: 'Hello World', done: false } console.log(gen.next()); // { value: 'end', done: true } console.log(gen.next()); // { value: undefined, done: true }
上面的程式碼一共呼叫了三次 next
方法,每次都返回一個包含執行資訊的物件,包含一個表示式的值和一個標記執行狀態的 flag
。
第一次呼叫 next
方法,遇到一個 yield
語句後停止,返回物件的 value
的值就是 yield
語句的值,done
屬性用來標誌 Generator
方法是否執行完畢。
第二次呼叫 next
方法,程式執行到 return
語句的位置,返回物件的 value
值即為 return
語句的值,如果沒有 return
語句,則會一直執行到函數結束,value
值為 undefined
,done
屬性值為 true
。
第三次呼叫 next
方法時,Generator
已經執行完畢,因此 value
的值為undefined
。
yield
本意為 生產 ,在 Python、Java 以及 C# 中都有 yield
關鍵字,但只有Python 中 yield
的語意相似(理由前面也說了)。
當 next
方法被呼叫時,Generator
函數開始向下執行,遇到 yield
關鍵字時,會暫停當前操作,並且對 yield
後的表示式進行求值,無論 yield
後面表示式返回的是何種型別的值,yield
操作最後返回的都是一個物件,該物件有 value
和 done
兩個屬性。
value
很好理解,如果後面是一個基本型別,那麼 value
的值就是對應的值,更為常見的是 yield
後面跟的是 Promise
物件。
done
屬性表示當前 Generator
物件的狀態,剛開始執行時 done
屬性的值為false
,當 Generator
執行到最後一個 yield
或者 return
語句時,done
的值會變成 true
,表示 Generator
執行結束。
注意:yield關鍵字本身不產生返回值。例如下面的程式碼:
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: undefined, done: true }
為什麼第二個 next
方法執行後,y
的值卻是 undefined
。
實際上,我們可以做如下理解:next
方法的返回值是 yield
關鍵字後面表示式的值,而 yield
關鍵字本身可以視為一個不產生返回值的函數,因此 y
並沒有被賦值。上面的例子中如果要計算 y
的值,可以將程式碼改成:
function* foo(x) { let y; yield y = x + 1; return 'end'; }
next
方法還可以接受一個數值作為引數,代表上一個 yield
求值的結果。
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next(10)); // { value: 10, done: true }
上面的程式碼等價於:
function* foo(x) { let y = yield(x + 1); y = 10; return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: 10, done: true }
next
可以接收引數代表可以從外部傳一個值到 Generator
函數內部,乍一看沒有什麼用處,實際上正是這個特性使得 Generator
可以用來組織非同步方法,我們會在後面介紹。
一個 Iterator
同樣使用 next
方法來遍歷元素。由於 Generator
函數會返回一個物件,而該物件實現了一個 Iterator
介面,因此所有能夠遍歷 Iterator
介面的方法都可以用來執行 Generator
,例如 for/of
、aray.from()
等。
可以使用 for/of
迴圈的方式來執行 Generator
函數內的步驟,由於 for/of
本身就會呼叫 next
方法,因此不需要手動呼叫。
注意:迴圈會在 done
屬性為 true
時停止,以下面的程式碼為例,最後的 'end'
並不會被列印出來,如果希望被列印,需要將最後的 return
改為 yield
。
function* Generator() { yield "Hello Node"; yield "From Lear" return "end" } const gen = Generator(); for (let i of gen) { console.log(i); } // 和 for/of 迴圈等價 console.log(Array.from(Generator()));;
前面提到過,直接列印 Generator
函數的範例沒有結果,但既然 Generator
函數返回了一個遍歷器,那麼就應該具有 Symbol.iterator
屬性。
console.log(gen[Symbol.iterator]);
// 輸出:[Function: [Symbol.iterator]]
Generator
函數的原型中定義了 throw
方法,用於丟擲異常。
function* generator() { try { yield console.log("Hello"); } catch (e) { console.log(e); } yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 輸出
// Hello
// throw error
// Node
上面程式碼中,執行完第一個 yield
操作後,Generator
物件丟擲了異常,然後被函數體中 try/catch
捕獲。當異常被捕獲後,Generator
函數會繼續向下執行,直到遇到下一個 yield
操作並輸出 yield
表示式的值。
function* generator() { try { yield console.log("Hello World"); } catch (e) { console.log(e); } console.log('test'); yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 輸出
// Hello World
// throw error
// test
// Node
如果 Generator
函數在執行的過程中出錯,也可以在外部進行捕獲。
function* generator() { yield console.log(undefined, undefined); return "end"; } const gen = generator(); try { gen.next(); } catch (e) { }
Generator
的原型物件還定義了 return()
方法,用來結束一個 Generator
函數的執行,這和函數內部的 return
關鍵字不是一個概念。
function* generator() { yield console.log('Hello World'); yield console.log('Hello 夏安'); return "end"; } const gen = generator(); gen.next(); // Hello World gen.return(); // return() 方法後面的 next 不會被執行 gen.next();
我們之所以可以使用 Generator
函數來處理非同步任務,原因有二:
Generator
函數可以中斷和恢復執行,這個特性由 yield
關鍵字來實現。Generator
函數內外可以交換資料,這個特性由 next
函數來實現。概括一下 Generator
函數處理非同步操作的核心思想:先將函數暫停在某處,然後拿到非同步操作的結果,然後再把這個結果傳到方法體內。
yield
關鍵字後面除了通常的函數表示式外,比較常見的是後面跟的是一個 Promise
,由於 yield
關鍵字會對其後的表示式進行求值並返回,那麼呼叫 next
方法時就會返回一個 Promise
物件,我們可以呼叫其 then
方法,並在回撥中使用 next
方法將結果傳回 Generator
。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data); });
上面的程式碼中,Generator
函數封裝了 readFile_promise
方法,該方法返回一個 Promise
,Generator
函數對 readFile_promise
的呼叫方式和同步操作基本相同,除了 yield
關鍵字之外。
上面的 Generator
函數中只有一個非同步操作,當有多個非同步操作時,就會變成下面的形式。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); const result2 = yield readFile_promise("bar.txt"); console.log(result2); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
然而看起來還是巢狀的回撥?難道使用 Generator
的初衷不是優化巢狀寫法嗎?說的沒錯,雖然在呼叫時保持了同步形式,但我們需要手動執行 Generator
函數,於是在執行時又回到了巢狀呼叫。這是 Generator
的缺點。
對 Generator
函數來說,我們也看到了要順序地讀取多個檔案,就要像上面程式碼那樣寫很多用來執行的程式碼。無論是 Promise
還是 Generator
,就算在編寫非同步程式碼時能獲得便利,但執行階段卻要寫更多的程式碼,Promise
需要手動呼叫 then
方法,Generator
中則是手動呼叫 next
方法。
當需要順序執行非同步操作的個數比較少的情況下,開發者還可以接受手動執行,但如果面對多個非同步操作就有些難辦了,我們避免了回撥地獄,卻又陷到了執行地獄裡面。我們不會是第一個遇到自動執行問題的人,社群已經有了很多解決方案,但為了更深入地瞭解 Promise
和 Generator
,我們不妨先試著獨立地解決這個問題,如何能夠讓一個 Generator
函數自動執行?
既然 Generator
函數是依靠 next
方法來執行的,那麼我們只要實現一個函數自動執行 next
方法不就可以了嗎,針對這種思路,我們先試著寫出這樣的程式碼:
function auto(generator) { const gen = generator(); while (gen.next().value !== undefined) { gen.next(); } }
思路雖然沒錯,但這種寫法並不正確,首先這種方法只能用在最簡單的 Generator
函數上,例如下面這種:
function* generator() { yield 'Hello World'; return 'end'; }
另一方面,由於 Generator
沒有 hasNext
方法,在 while
迴圈中作為條件的:gen.next().value !== undefined
在第一次條件判斷時就開始執行了,這表示我們拿不到第一次執行的結果。因此這種寫法行不通。
那麼換一種思路,我們前面介紹了 for/of
迴圈,那麼也可以用它來執行 Generator
。
function* Generator() { yield "Hello World"; yield "Hello 夏安"; yield "end"; } const gen = Generator(); for (let i of gen) { console.log(i); }
// 輸出結果
// Hello World
// Hello 夏安
// end
看起來沒什麼問題了,但同樣地也只能拿來執行最簡單的 Generator
函數,然而我們的主要目的還是管理非同步操作。
前面實現的執行器都是針對普通的 Generator
函數,即裡面沒有包含非同步操作,在實際應用中,yield
後面跟的大都是 Promise
,這時候 for/of
實現的執行器就不起作用了。
通過觀察,我們發現 Generator
的巢狀執行是一種遞迴呼叫,每一次的巢狀的返回結果都是一個 Promise
物件。
const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
那麼,我們可以根據這個寫出新的執行函數。
function autoExec(gen) { function next(data) { const result = gen.next(data); // 判斷執行是否結束 if (result.done) return result.value; result.value.then(function (data) { next(data); }); } next(); }
這個執行器因為呼叫了 then
方法,因此只適用於 yield
後面跟一個 Promise
的方法。
為了解決 generator
執行的問題,TJ 於2013年6月釋出了著名 co
模組,這是一個用來自動執行 Generator
函數的小工具,和 Generator
配合可以實現接近同步的呼叫方式,co
方法仍然會返回一個 Promise
。
const co = require("co"); function* gen() { const result = yield readFilePromise("foo.txt"); console.log(result); const result2 = yield readFilePromise("bar.txt"); console.log(result2); } co(gen);
只要將 Generator
函數作為引數傳給 co
方法就能將內部的非同步任務順序執行,要使用 co
模組,yield
後面的語句只能是 promsie
物件。
到此為止,我們對非同步的處理有了一個比較妥當的方式,利用 generator+co
,我們基本可以用同步的方式來書寫非同步操作了。但 co
模組仍有不足之處,由於它仍然返回一個 Promise
,這代表如果想要獲得非同步方法的返回值,還要寫成下面這種形式:
co(gen).then(function (value) { console.log(value); });
另外,當面對多個非同步操作時,除非將所有的非同步操作都放在一個 Generator
函數中,否則如果需要對 co
的返回值進行進一步操作,仍然要將程式碼寫到 Promise
的回撥中去。
到此這篇關於JavaScript Generator非同步過度的實現詳解的文章就介紹到這了,更多相關JavaScript Generator 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45