前端人不了解的迭代器和生成器
迭代器和生成器是 ES6 中引入的特性。迭代器通過一次消費(fèi)一個(gè)項(xiàng)目列表來提高效率,類似于數(shù)據(jù)流。生成器是一種能夠暫停執(zhí)行的特殊函數(shù)。調(diào)用生成器允許以塊的形式(一次一個(gè))生成數(shù)據(jù),而無需先將其存儲(chǔ)在列表中。下面就來深入理解 JavaScript 中的迭代器和生成器,看看它們是如何使用的,又有何妙用!
迭代器
JavaScript 中的迭代器可以分別兩種:同步迭代器和異步迭代器。
1. 同步迭代器
(1)迭代器和可迭代對(duì)象
在 JavaScript 中有很多方法可以遍歷數(shù)據(jù)結(jié)構(gòu)。例如,使用 for 循環(huán)或使用 while 循環(huán)。迭代器具有類似的功能,但有顯著差異。
迭代器只需要知道集合中的當(dāng)前位置,而其他循環(huán)則需要預(yù)先加載整個(gè)集合才能循環(huán)遍歷它。迭代器使用 next() 方法訪問集合中的下一個(gè)元素。但是,為了使用迭代器,值或數(shù)據(jù)結(jié)構(gòu)應(yīng)該是可迭代的。數(shù)組、字符串、映射、集合是 JavaScript 中的可迭代對(duì)象。普通對(duì)象是不可迭代的。
(2)定義迭代器
下面來看看集合不可迭代的場(chǎng)景:
const favouriteMovies = {
a: '哈利波特',
b: '指環(huán)王',
c: '尖峰時(shí)刻',
d: '星際穿越',
e: '速度與激情',
}
這個(gè)對(duì)象是不可迭代的。如果使用普通的 for 循環(huán)遍歷它,就會(huì)拋出錯(cuò)誤。隨著 ES6 中迭代器的引入,可以將其轉(zhuǎn)換為可迭代對(duì)象以便遍歷它。這些稱為自定義迭代器。下面看看如何實(shí)現(xiàn)對(duì)象的遍歷并打印出來:
favouriteMovies[Symbol.iterator] = function() {
const ordered = Object.values(this).sort((a, b) => a - b);
let i = 0;
return {
next: () => ({
done: i >= ordered.length,
value: ordered[i++]
})
}
}
for (const v of favouriteMovies) {
console.log(v);
}
輸出結(jié)果如下:
哈利波特
指環(huán)王
尖峰時(shí)刻
星際穿越
速度與激情
這里使用 Symbol.iterator() 來定義迭代器。任何具有 Symbol.iterator 鍵的結(jié)構(gòu)都是可迭代的。
可迭代對(duì)象具有以下行為:
- 當(dāng) for..of 循環(huán)開始時(shí),它首先查找錯(cuò)誤。如果未找到,則它會(huì)訪問方法和定義該方法的對(duì)象。
- 以 for..of 循環(huán)方式迭代該對(duì)象。
- 使用該輸出對(duì)象的 next() 方法來獲取要返回的下一個(gè)值。
- 返回的值的格式為 done:boolean, value: any。返回 done:true 時(shí)循環(huán)結(jié)束。
下面來創(chuàng)建一個(gè) LeapYear 對(duì)象,該對(duì)象返回范圍為 (start, end) 的閏年列表,并在后續(xù)閏年之間設(shè)置間隔。
class LeapYear {
constructor(start = 2020, end = 2040, interval = 4) {
this.start = start;
this.end = end;
this.interval = interval;
}
[Symbol.iterator]() {
let nextLeapYear = this.start;
return {
next: () => {
if (nextLeapYear <= this.end) {
let result = { value: nextLeapYear, done: false };
nextLeapYear += this.interval;
return result;
}
return { value: undefined, done: true };
},
};
}
}
在上面的代碼中,為自定義類型 LeapYear 實(shí)現(xiàn)了 Symbol.iterator() 方法。分別在 this.start 和 this.end 字段中有迭代的起點(diǎn)和終點(diǎn)。使用 this.interval來跟蹤迭代的第一個(gè)元素和下一個(gè)元素之間的間隔。
現(xiàn)在,可以在自定義類型上調(diào)用 for...of 循環(huán),并查看其行為和輸出值,就像默認(rèn)數(shù)組類型一樣:
let leapYears = new LeapYear();
for (const leapYear of leapYears) {
console.log(leapYear);
}
輸出結(jié)果如下:
2020
2024
2028
2032
2036
2040
這里的 LeapYear 通過 Symbol.iterator() 變成了可迭代對(duì)象。
在一些情況下,迭代器會(huì)比普通迭代更好。例如,在沒有隨機(jī)訪問的有序集合(如數(shù)組)中,迭代器的性能會(huì)更好,因?yàn)樗梢灾苯痈鶕?jù)當(dāng)前位置檢索元素。但是,對(duì)于無序集合,由于沒有順序,就不會(huì)體驗(yàn)到性能上的重大差異。
使用普通循環(huán)算法,例如 for 循環(huán)或 while 循環(huán),您只能循環(huán)遍歷允許迭代的集合:
const favourtieMovies = [
'哈利波特',
'指環(huán)王',
'尖峰時(shí)刻',
'星際穿越',
'速度與激情',
];
for (let i=0; i < favourtieMovies.length; i++) {
console.log(favouriteMovies[i]);
}
let i = 0;
while (i < favourtieMovies.length) {
console.log(favourtieMovies[i]);
i++;
}
由于數(shù)組是可迭代的,因此可以使用 for 循環(huán)遍歷。我們也可以為上面實(shí)現(xiàn)一個(gè)迭代器,這將允許更好地訪問基于當(dāng)前位置的元素,而無需加載整個(gè)集合。代碼如下:
const iterator = favourtieMovies[Symbol.iterator]();
iterator.next(); // { value: '哈利波特', done: false }
iterator.next(); // { value: '指環(huán)王', done: false }
iterator.next(); // { value: '尖峰時(shí)刻', done: false }
iterator.next(); // { value: '星際穿越', done: false }
iterator.next(); // { value: '速度與激情', done: false }
iterator.next(); // { value: undefined, done: true }
next() 方法將返回迭代器的結(jié)果。它包括兩個(gè)值;集合中的元素和完成狀態(tài)。可以看到,當(dāng)遍歷完成后,即使訪問數(shù)組外的元素,也不會(huì)拋出錯(cuò)誤。它只會(huì)返回一個(gè)具有 undefined 值和完成狀態(tài)為 true 的對(duì)象。
(3)使用場(chǎng)景
那為什么向自定義對(duì)象中添加迭代器呢?我們也可以編寫自定義函數(shù)來遍歷對(duì)象以完成同樣的事情。
實(shí)際上,迭代器是一種標(biāo)準(zhǔn)化自定義對(duì)象的優(yōu)雅實(shí)現(xiàn)方式,它為自定義數(shù)據(jù)結(jié)構(gòu)提供了一種在更大的 JS 環(huán)境中很好地工作的方法。因此,提供自定義數(shù)據(jù)結(jié)構(gòu)的庫(kù)經(jīng)常會(huì)使用迭代器。例如, Immutable.JS 庫(kù)就使用迭代器為其自定義對(duì)象(如Map)。所以,如果需要為封裝良好的自定義數(shù)據(jù)結(jié)構(gòu)提供原生迭代功能,就考慮使用迭代器。
2. 異步迭代器
JavaScript 中的異步迭代對(duì)象是實(shí)現(xiàn) Symbol.asyncIterator 的對(duì)象:
const asyncIterable = {
[Symbol.asyncIterator]: function() {
}
};
我們可以將一個(gè)函數(shù)分配給 [Symbol.asyncIterator] 以返回一個(gè)迭代器對(duì)象。迭代器對(duì)象應(yīng)符合帶有 next() 方法的迭代器協(xié)議(類似于同步迭代器)。
下面來添加迭代器:
const asyncIterable = {
[Symbol.asyncIterator]: function() {
let count = 0;
return {
next() {
count++;
if (count <= 3) {
return Promise.resolve({ value: count, done: false });
}
return Promise.resolve({ value: count, done: true });
}
};
}
};
這里用 Promise.resolve 包裝了返回的對(duì)象。下面來執(zhí)行 next() 方法:
const go = asyncIterable[Symbol.asyncIterator]();
go.next().then(iterator => console.log(iterator.value));
go.next().then(iterator => console.log(iterator.value));
輸出結(jié)果如下:
1
2
也可以使用 for await...of 來對(duì)異步迭代對(duì)象進(jìn)行迭代:
async function consumer() {
for await (const asyncIterableElement of asyncIterable) {
console.log(asyncIterableElement);
}
}
consumer();
異步迭代器和迭代器是異步生成器的基礎(chǔ),后面會(huì)介紹異步生成器。
生成器
JavaScript 中的生成器可以分別兩種:同步生成器和異步生成器。
1. 同步生成器
(1)基本概念
生成器是一個(gè)可以暫停和恢復(fù)并可以產(chǎn)生多個(gè)值的過程。JavaScript 中的生成器由一個(gè)生成器函數(shù)組成,它返回一個(gè)可迭代 Generator 對(duì)象。
生成器是對(duì) JavaScript 的強(qiáng)大補(bǔ)充。它們可以維護(hù)狀態(tài),提供一種制作迭代器的有效方法,并且能夠處理無限數(shù)據(jù)流,可用于在前端實(shí)現(xiàn)無限滾動(dòng)等。此外,當(dāng)與 Promises 一起使用時(shí),生成器可以模擬 async/await 功能,這使我們能夠以更直接和可讀的方式處理異步代碼。盡管 async/await 是處理常見、簡(jiǎn)單的異步用例(例如從 API 獲取數(shù)據(jù))的一種更普遍的方式,但生成器具有更高級(jí)的功能。
生成器函數(shù)是返回生成器對(duì)象的函數(shù),由 function 關(guān)鍵字后面跟星號(hào) (*) 定義,如下所示:
function* generatorFunction() {}
有時(shí),我們可能會(huì)在函數(shù)名稱旁邊看到星號(hào),而不是 function 關(guān)鍵字,例如 function *generatorFunction(),它的工作原理是相同的,但 function* 是一種更廣泛接受的語法。
生成器函數(shù)也可以在表達(dá)式中定義,就像常規(guī)函數(shù)一樣:
const generatorFunction = function* () {}
生成器甚至可以是對(duì)象或類的方法:
// 生成器作為對(duì)象的方法
const generatorObj = {
*generatorMethod() {},
}
// 生成器作為類的方法
class GeneratorClass {
*generatorMethod() {}
}
下面的例子都將使用生成器函數(shù)聲明得語法。
注意:與常規(guī)函數(shù)不同,生成器不能使用 new 關(guān)鍵字構(gòu)造,也不能與箭頭函數(shù)結(jié)合使用。
現(xiàn)在我們知道了如何聲明生成器函數(shù),下面來看看生成器返回的可迭代生成器對(duì)象。
(2)生成器對(duì)象
傳統(tǒng)的 JavaScript 函數(shù)會(huì)在遇到return 關(guān)鍵字時(shí)返回一個(gè)值。如果省略 return 關(guān)鍵字,函數(shù)將隱式返回 undefined。
例如,在下面的代碼中,我們聲明了一個(gè) sum() 函數(shù),它返回一個(gè)值,該值是兩個(gè)整數(shù)參數(shù)的和:
function sum(a, b) {
return a + b
}
調(diào)用該函數(shù)會(huì)返回一個(gè)值,該值是參數(shù)的總和:
const value = sum(5, 6) // 11
而生成器函數(shù)不會(huì)立即返回值,而是返回一個(gè)可迭代的生成器對(duì)象。在下面的例子中,我們聲明了一個(gè)函數(shù)并給它一個(gè)單一的返回值,就像一個(gè)標(biāo)準(zhǔn)的函數(shù):
function* generatorFunction() {
return 'Hello, Generator!'
}
當(dāng)調(diào)用生成器函數(shù)時(shí),它將返回生成器對(duì)象,我們可以將其分配給一個(gè)變量:
const generator = generatorFunction()
如果這是一個(gè)常規(guī)函數(shù),我們希望生成器為我們提供函數(shù)中返回的字符串。然而,我們實(shí)際得到的是一個(gè)處于掛起狀態(tài)的對(duì)象。因此,調(diào)用生成器將提供類似于以下內(nèi)容的輸出:
generatorFunction {<suspended>}
[[GeneratorLocation]]: VM335:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ?* generatorFunction()
[[GeneratorReceiver]]: Window
函數(shù)返回的生成器對(duì)象是一個(gè)迭代器。迭代器是一個(gè)具有可用的 next() 方法的對(duì)象,該方法用于迭代一系列值。next() 方法返回一個(gè)對(duì)象,其包含兩個(gè)屬性:
- value:當(dāng)前步驟的值;
- done:布爾值,指示生成器中是否有更多值。
next() 方法必須遵循以下規(guī)則:
- 返回一個(gè)帶有 done: false 的對(duì)象來繼續(xù)迭代;
- 返回一個(gè)帶有 done: true 的對(duì)象來停止迭代。
下面就來在生成器上調(diào)用 next() 并獲取迭代器的當(dāng)前值和狀態(tài):
generator.next()
這將得到以下輸出結(jié)果:
{value: "Hello, Generator!", done: true}
調(diào)用 next() 時(shí)的返回值為 Hello, Generator!,并且 done 的狀態(tài)為 true,因?yàn)樵撝祦碜躁P(guān)閉迭代器的返回值。由于迭代器完成,生成器函數(shù)的狀態(tài)將從掛起變?yōu)殛P(guān)閉。這時(shí)再次調(diào)用生成器將輸出以下內(nèi)容:
generatorFunction {<closed>}
除此之外,生成器函數(shù)也有區(qū)別于普通函數(shù)的獨(dú)特特征。下面我們就來了解一下 yield 運(yùn)算符并看看生成器如何暫停和恢復(fù)執(zhí)行。
(3)yield 運(yùn)算符
生成器為 JavaScript 引入了一個(gè)新的關(guān)鍵字:yield。**yield**** 可以暫停生成器函數(shù)并返回 **yield** 之后的值,從而提供一種輕量級(jí)的方法來遍歷值。**
在下面的例子中,我們將使用不同的值暫停生成器函數(shù)三次,并在最后返回一個(gè)值。然后將生成器對(duì)象分配給 generator 變量。
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
return 'Hello, Generator!'
}
const generator = generatorFunction()
現(xiàn)在,當(dāng)我們?cè)谏善骱瘮?shù)上調(diào)用 next() 時(shí),它會(huì)在每次遇到 yield 時(shí)暫停。done 會(huì)在每次 yield 后設(shè)置為 false,表示生成器還沒有結(jié)束。一旦遇到 return,或者函數(shù)中沒有更多的 yield 時(shí),done 就會(huì)變?yōu)?nbsp;true,生成器函數(shù)就結(jié)束了。
連續(xù)四次調(diào)用 next() 方法:
generator.next()
generator.next()
generator.next()
generator.next()
這些將按順序得到以下結(jié)果:
{value: "One", done: false}
{value: "Two", done: false}
{value: "Three", done: false}
{value: "Hello, Generator!", done: true}
next() 非常適合從迭代器對(duì)象中提取有限數(shù)據(jù)。
注意,生成器不需要 return;如果省略,最后一次迭代將返回 {value: undefined, done: true},生成器完成后對(duì) next() 的任何后續(xù)調(diào)用也是如此。
(4)遍歷生成器
使用 next() 方法可以遍歷生成器對(duì)象,接收完整對(duì)象的所有 value 和 done 屬性。不過,就像 Array、Map 和 Set 一樣,Generator 遵循迭代協(xié)議,并且可以使用 for...of 進(jìn)行迭代:
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
return 'Hello, Generator!'
}
const generator = generatorFunction()
for (const value of generator) {
console.log(value)
}
輸出結(jié)果如下:
One
Two
Three
擴(kuò)展運(yùn)算符也可用于將生成器的值分配給數(shù)組:
const values = [...generator]
console.log(values)
輸出結(jié)果如下:
['One', 'Two', 'Three']
可以看到,擴(kuò)展運(yùn)算符和 for...of 都不會(huì)將 return 的值計(jì)入 value。
注意:雖然這兩種方法對(duì)于有限生成器都是有效的,但如果生成器正在處理無限數(shù)據(jù)流,則無法在不創(chuàng)建無限循環(huán)的情況下直接使用擴(kuò)展運(yùn)算符或 for...of。
我們還可以從迭代結(jié)果中解構(gòu)值:
const [a, b, c]= generator;
console.log(a);
console.log(b);
console.log(c);
輸出結(jié)果如下:
One
Two
Three
(5)關(guān)閉生成器
如我們所見,生成器可以通過遍歷其所有值將其 done 屬性設(shè)置為 true 并將其狀態(tài)設(shè)置為 closed 。除此之外,還有兩種方法可以立即關(guān)閉生成器:使用 return() 方法和使用 throw() 方法。
使用 return(),生成器可以在任何時(shí)候終止,就像在函數(shù)體中的 return 語句一樣??梢詫?shù)傳遞給 return(),或?qū)⑵淞艨找员硎疚炊x的值。
下面來創(chuàng)建一個(gè)具有 yield 值但在函數(shù)定義中沒有 return 的生成器:
function* generatorFunction() {
yield 'One'
yield 'Two'
yield 'Three'
}
const generator = generatorFunction()
第一個(gè) next() 將返回“One”,并將 done 設(shè)置為 false。如果在那之后立即在生成器對(duì)象上調(diào)用 return() 方法,將獲得傳遞的值并將 done 設(shè)置為 true。對(duì) next() 的任何額外調(diào)用都會(huì)給出默認(rèn)的已完成生成器響應(yīng),其中包含一個(gè) undefined 值。
generator.next()
generator.return('Return!')
generator.next()
輸出結(jié)果如下:
{value: "Neo", done: false}
{value: "Return!", done: true}
{value: undefined, done: true}
return() 方法會(huì)強(qiáng)制生成器對(duì)象完成并忽略任何其他 yield 關(guān)鍵字。當(dāng)需要使函數(shù)可取消時(shí),這在異步編程中特別有用,例如當(dāng)用戶想要執(zhí)行不同的操作時(shí)中斷數(shù)據(jù)請(qǐng)求,因?yàn)闊o法直接取消 Promise。
如果生成器函數(shù)的主體有捕獲和處理錯(cuò)誤的方法,則可以使用 throw() 方法將錯(cuò)誤拋出到生成器中。這將啟動(dòng)生成器,拋出錯(cuò)誤并終止生成器。
下面來在生成器函數(shù)體內(nèi)放一個(gè) try...catch 并在發(fā)現(xiàn)錯(cuò)誤時(shí)記錄錯(cuò)誤:
function* generatorFunction() {
try {
yield 'One'
yield 'Two'
} catch (error) {
console.log(error)
}
}
const generator = generatorFunction()
現(xiàn)在來運(yùn)行 next() 方法,然后運(yùn)行 throw() 方法:
generator.next()
generator.throw(new Error('Error!'))
輸出結(jié)果如下:
{value: "One", done: false}
Error: Error!
{value: undefined, done: true}
使用 throw() 可以將錯(cuò)誤注入到生成器中,該錯(cuò)誤被 try...catch 捕獲并記錄到控制臺(tái)。
(6)生成器對(duì)象方法和狀態(tài)
下面是生成器對(duì)象的方法:
- next():返回生成器中的后面的值;
- return():在生成器中返回一個(gè)值并結(jié)束生成器;
- throw():拋出錯(cuò)誤并結(jié)束生成器。
下面是生成器對(duì)象的狀態(tài):
- suspended:生成器已停止執(zhí)行但尚未終止。
- closed:生成器因遇到錯(cuò)誤、返回或遍歷所有值而終止。
(7)yield 委托
除了常規(guī)的 yield 運(yùn)算符之外,生成器還可以使用 yield* 表達(dá)式將更多值委托給另一個(gè)生成器。當(dāng)在生成器中遇到 yield* 時(shí),它將進(jìn)入委托生成器并開始遍歷所有 yield 直到該生成器關(guān)閉。這可以用于分離不同的生成器函數(shù)以在語義上組織代碼,同時(shí)仍然讓它們的所有 **yield** 都可以按正確的順序迭代。
下面來創(chuàng)建兩個(gè)生成器函數(shù),其中一個(gè)將對(duì)另一個(gè)進(jìn)行 yield* 操作:
function* delegate() {
yield 3
yield 4
}
function* begin() {
yield 1
yield 2
yield* delegate()
}
接下來,遍歷 begin() 生成器函數(shù):
const generator = begin()
for (const value of generator) {
console.log(value)
}
輸出結(jié)果如下:
1
2
3
4
外部的生成器(begin)生成值 1 和 2,然后使用 yield* 委托給另一個(gè)生成器(delegate),返回 3 和 4。
yield* 還可以委托給任何可迭代的對(duì)象,例如 Array 或 Map。yield 委托有助于組織代碼,因?yàn)樯善髦腥魏蜗胍褂?nbsp;yield 的函數(shù)也必須是一個(gè)生成器。
(8)在生成器中傳遞值
上面的例子中,我們使用生成器作為迭代器,并且在每次迭代中產(chǎn)生值。除了產(chǎn)生值之外,生成器還可以使用 next() 中的值。在這種情況下,yield 將包含一個(gè)值。
需要注意,調(diào)用的第一個(gè) next() 不會(huì)傳遞值,而只會(huì)啟動(dòng)生成器。為了證明這一點(diǎn),可以記錄 yield 的值并使用一些值調(diào)用 next() 幾次。
function* generatorFunction() {
console.log(yield)
console.log(yield)
return 'End'
}
const generator = generatorFunction()
generator.next()
generator.next(100)
generator.next(200)
輸出結(jié)果如下:
100
200
{value: "End", done: true}
除此之外,也可以為生成器提供初始值。下面來創(chuàng)建一個(gè) for 循環(huán)并將每個(gè)值傳遞給 next() 方法,同時(shí)將一個(gè)參數(shù)傳遞給 inital 函數(shù):
function* generatorFunction(value) {
while (true) {
value = yield value * 10
}
}
const generator = generatorFunction(0)
for (let i = 0; i < 5; i++) {
console.log(generator.next(i).value)
}
這將從 next() 中檢索值并為下一次迭代生成一個(gè)新值,該值是前一個(gè)值乘以 10。輸出結(jié)果如下:
0
10
20
30
40
處理啟動(dòng)生成器的另一種方法是將生成器包裝在一個(gè)函數(shù)中,該函數(shù)將會(huì)在執(zhí)行任何其他操作之前調(diào)用 next() 一次。
(9)async/await
async/await 使處理異步數(shù)據(jù)更簡(jiǎn)單、更容易理解。生成器具有比異步函數(shù)更廣泛的功能,但能夠復(fù)制類似的行為。以這種方式實(shí)現(xiàn)異步編程可以增加代碼的靈活性。
下面來構(gòu)建一個(gè)異步函數(shù),它使用 Fetch API 獲取數(shù)據(jù)并將響應(yīng)記錄到控制臺(tái)。
首先定義一個(gè)名為 getUsers 的異步函數(shù),該函數(shù)從 API 獲取數(shù)據(jù)并返回一個(gè)對(duì)象數(shù)組,然后調(diào)用 getUsers:
const getUsers = async function () {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const json = await response.json()
return json
}
getUsers().then((response) => console.log(response))
輸出結(jié)果如下:
圖片
使用生成器可以創(chuàng)建幾乎相同但不使用 async/await 關(guān)鍵字的效果。相反,它將使用我們創(chuàng)建的新函數(shù),并產(chǎn)生值而不是等待 Promise。
const getUsers = asyncAlt(function* () {
const response = yield fetch('https://jsonplaceholder.typicode.com/users')
const json = yield response.json()
return json
})
getUsers().then((response) => console.log(response))
如我們所見,它看起來與 async/await 實(shí)現(xiàn)幾乎相同,除了有一個(gè)生成器函數(shù)被傳入以產(chǎn)生值。
現(xiàn)在可以創(chuàng)建一個(gè)類似于異步函數(shù)的 asyncAlt 函數(shù)。asyncAlt 有一個(gè) generatorFunction 參數(shù),它是產(chǎn)生 fetch 返回的 Promise 的函數(shù)。asyncAlt 返回函數(shù)本身,并 resolve 它得到的每個(gè) Promise,直到最后一個(gè):
function asyncAlt(generatorFunction) {
return function () {
// 創(chuàng)建并分配生成器對(duì)象
const generator = generatorFunction()
// 定義一個(gè)接受生成器下一次迭代的函數(shù)
function resolve(next) {
// 如果生成器關(guān)閉并且沒有更多的值可以生成,則解析最后一個(gè)值
if (next.done) {
return Promise.resolve(next.value)
}
// 如果仍有值可以產(chǎn)生,那么它們就是Promise,必須 resolved。
return Promise.resolve(next.value).then((response) => {
return resolve(generator.next(response))
})
}
// 開始 resolve Promise
return resolve(generator.next())
}
}
這樣就會(huì)得到和async/await一樣的結(jié)果:
圖片
盡管這個(gè)方法可以為代碼增加靈活性,但通常 async/await 是更好的選擇,因?yàn)樗橄罅藢?shí)現(xiàn)細(xì)節(jié)并讓開發(fā)者專注于編寫高效代碼。
(10)使用場(chǎng)景
很多開發(fā)人員認(rèn)為生成器函數(shù)視為一種奇特的 JavaScript 功能,在現(xiàn)實(shí)中幾乎沒有應(yīng)用。在大多數(shù)情況下,確實(shí)用不到生成器。
生成器的優(yōu)點(diǎn):
- 惰性求值:除非需要,否則不計(jì)算值。它提供按需計(jì)算。只有需要它時(shí),value 才會(huì)存在。
- 內(nèi)存效率高:由于惰性求值,生成器的內(nèi)存效率非常高,因?yàn)樗粫?huì)為預(yù)先生成的未使用值分配不必要的內(nèi)存位置。
- 更簡(jiǎn)潔的代碼:生成器提供更簡(jiǎn)潔的代碼,尤其是在異步行為中。
生成器在對(duì)性能要求高的場(chǎng)景中有很大的用處。特別是,它們適用于以下場(chǎng)景:
- 處理大文件和數(shù)據(jù)集。
- 生成無限的數(shù)據(jù)序列。
- 按需計(jì)算昂貴的邏輯。
Redux sagas 就是實(shí)踐中使用的生成器的一個(gè)很好的例子。它是一個(gè)用于管理redux應(yīng)用異步操作的中間件,redux-saga 通過創(chuàng)建 sagas 將所有異步操作邏輯收集在一個(gè)地方集中處理,可以用來代替 redux-thunk 中間件。
2. 異步生成器
ECMAScript 2018 中引入了異步生成器的概念,它是一種特殊類型的異步函數(shù),可以隨意停止和恢復(fù)其執(zhí)行。
同步生成器函數(shù)和異步生成器函數(shù)的區(qū)別在于,后者從迭代器對(duì)象返回一個(gè)異步的、基于 Promise 的結(jié)果。
要想創(chuàng)建異步生成器函數(shù),需要聲明一個(gè)帶有星號(hào) * 的生成器函數(shù),前綴為 async:
async function* asyncGenerator() {
}
一旦進(jìn)入函數(shù),就可以使用 yield 來暫停執(zhí)行:
async function* asyncGenerator() {
yield 'One'
yield 'Two'
}
這里 yield 會(huì)暫停執(zhí)行并返回一個(gè)迭代器對(duì)象給調(diào)用者。這個(gè)對(duì)象既是可迭代對(duì)象,又是迭代器。
異步生成器函數(shù)不會(huì)像常規(guī)函數(shù)那樣在一步中計(jì)算出所有結(jié)果。相反,它會(huì)逐步提取值。我們可以使用兩種方法從異步生成器解析 Promise:
- 在迭代器對(duì)象上調(diào)用 next();
- 使用 for await...of 異步迭代。
對(duì)于上面的例子,可以這樣做:
async function* asyncGenerator() {
yield 'One';
yield 'Two';
}
const go = asyncGenerator();
go.next().then(iterator => console.log(iterator.value));
go.next().then(iterator => console.log(iterator.value));
輸出結(jié)果如下:
'One';
'Two'
另一種方法使用異步迭代 for await...of。要使用異步迭代,需要用 async 函數(shù)包裝它:
async function* asyncGenerator() {
yield 'One';
yield 'Two';
}
async function consumer() {
for await (const value of asyncGenerator()) {
console.log(value);
}
}
consumer();
for await...of 非常適合提取非有限數(shù)據(jù)流。