JavaScript Generator異步過度的實現詳解
異步過渡方案Generator
在使用 Generator
前,首先知道 Generator
是什麼。
如果讀者有 Python 開發經驗,就會發現,無論是概念還是形式上,ES2015 中的 Generator
幾乎就是 Python 中 Generator
的翻版。
Generator
本質上是一個函數,它最大的特點就是可以被中斷,然後恢復執行。通常來說,當開發者調用一個函數之後,這個函數的執行就脫離瞭開發者的控制,隻有函數執行完畢之後,控制權才能重新回到調用者手中,因此程序員在編寫方法代碼時,唯一
能夠影響方法執行的隻有預先定義的 return
關鍵字。
Promise
也是如此,我們也無法控制 Promise
的執行,新建一個 Promise
後,其狀態自動轉換為 pending
,同時開始執行,直到狀態改變後我們才能進行下一步操作。
而 Generator
函數不同,Generator
函數可以由用戶執行中斷或者恢復執行的操作,Generator
中斷後可以轉去執行別的操作,然後再回過頭從中斷的地方恢復執行。
1. 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
語句)。
2. Generator 函數的執行
跟普通函數相比,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
。
2.1 yield 關鍵字
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
可以用來組織異步方法,我們會在後面介紹。
2.2 next 方法與 Iterator 接口
一個 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]]
3. Generator 中的錯誤處理
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();
4. 用 Generator 組織異步方法
我們之所以可以使用 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
的缺點。
5. Generator 的自動執行
對 Generator
函數來說,我們也看到瞭要順序地讀取多個文件,就要像上面代碼那樣寫很多用來執行的代碼。無論是 Promise
還是 Generator
,就算在編寫異步代碼時能獲得便利,但執行階段卻要寫更多的代碼,Promise
需要手動調用 then
方法,Generator
中則是手動調用 next
方法。
當需要順序執行異步操作的個數比較少的情況下,開發者還可以接受手動執行,但如果面對多個異步操作就有些難辦瞭,我們避免瞭回調地獄,卻又陷到瞭執行地獄裡面。我們不會是第一個遇到自動執行問題的人,社區已經有瞭很多解決方案,但為瞭更深入地瞭解 Promise
和 Generator
,我們不妨先試著獨立地解決這個問題,如何能夠讓一個 Generator
函數自動執行?
5.1 自動執行器的實現
既然 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
函數,然而我們的主要目的還是管理異步操作。
5.2 基於Promise的執行器
前面實現的執行器都是針對普通的 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
的方法。
5.3 使用 co 模塊來自動執行
為瞭解決 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 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JavaScript前端迭代器Iterator與生成器Generator講解
- await-to-js源碼深入理解處理異步任務用法示例
- 詳解ES9的新特性之異步遍歷Async iteration
- 淺析Promise的介紹及基本用法
- JavaScript詳解使用Promise處理回調地獄與async await修飾符