您好,登錄后才能下訂單哦!
這篇文章主要講解了“JavaScript Generator異步過度如何實現”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“JavaScript Generator異步過度如何實現”吧!
在使用 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異步過度如何實現這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。