前言
ES6 中提出一個叫生成器(Generator)的概念,執行生成器函式,會返回迭代器物件(Iterator),這個迭代器物件可以遍歷函式內部的每一個狀態。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
// 通過執行生成器返回迭代器物件
var helloWorldIterator = helloWorldGenerator();
helloWorldIterator.next();
// { value: "hello", done: false }
helloWorldIterator.next();
// { value: "world", done: false }
helloWorldIterator.next();
// { value: "ending", done: true }
helloWorldIterator.next();
// { value: undefined, done: true }
複製程式碼
迭代器物件通過呼叫 next() 方法,遍歷下一個內部狀態,生成一個值,這也是 Generator 名字的由來。
一、generator 的非同步呼叫
每當 generator 生成一個值,程式會掛起,自動停止執行,隨後等待下一次執行,直到下一次呼叫 next() 方法,但並不影響外部主執行緒其他函式的執行。
generator 讓函式執行過程有了同步的特點,基於這個特點,我們將非同步呼叫和生成器結合起來:
先後列印 "hello China!", "hello Wolrd!", "hello Earth!";
function fetch(word) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello " + word);
}, 2000)
})
}
function* gen() {
try {
const api1 = yield fetch("China!");
console.log(1);
const api2 = yield fetch("World!");
console.log(2);
const api3 = yield fetch("Earth!");
console.log(3);
} catch(error) {
console.log(error);
}
}
const iterator = gen(); // 返回迭代器物件
const result1 = iterator.next().value;
result1
.then(res1 => {
console.log(res1)
return iterator.next().value;
})
.then(res2 => {
console.log(res2)
return iterator.next().value;
})
.then(res3 => {
console.log(res3)
return iterator.next().value;
})
複製程式碼
每次呼叫迭代器的 next 方法,會返回一個 Promise 物件,通過 Promise 物件狀態從 pending 轉移到 fullfilled 狀態,可以在 .then() 方法後執行下一個非同步方法。
二、Generator 自執行
從第二節中可以看出,Generator 每次呼叫非同步方法,都要手動執行一次 iterator.next(),通過遞迴 iterator.next() 我們就不用再手動執行 next() 方法了。
function fetch(word) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello " + word);
}, 2000)
})
}
function* gen() {
try {
const api1 = yield fetch("China!");
console.log(1);
const api2 = yield fetch("World!");
console.log(2);
const api3 = yield fetch("Earth!");
console.log(3);
} catch(error) {
console.log(error);
}
}
function co(gen) {
const g = gen();
function next(data) {
const result = g.next(data);
if(result.done) return;
result.value.then(data => {
console.log(data);
next(data);
})
}
next();
}
co(gen);
複製程式碼
三、在迭代器中丟擲錯誤
迭代器除了能在 next() 方法中傳遞引數外,還能通過 iterator.throw 方法捕捉到錯誤,從而增強了非同步程式設計的錯誤處理能力。
function fetch(word) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello " + word);
}, 2000)
})
}
function* gen() {
try {
const api1 = yield fetch("China!");
console.log(1);
const api2 = yield fetch("World!");
console.log(2);
const api3 = yield fetch("Earth!");
console.log(3);
} catch(error) {
console.log(error); // Error: 丟擲一個錯誤
}
}
const iterator = gen(); // 返回迭代器物件
const result1 = iterator.next().value;
result1
.then(res1 => {
console.log(res1)
iterator.throw(new Error("丟擲一個錯誤"))
return iterator.next().value;
})
.then(res2 => {
console.log(res2)
return iterator.next().value;
})
.then(res3 => {
console.log(res3)
return iterator.next().value;
})
複製程式碼
呼叫了 iterator.throw 方法後,錯誤就能被丟擲被生成器中的中的 try catch 捕捉到,且阻止後面的程式碼繼續執行。
四、Generator 應用場景
Generator 最令人興奮的地方在於,生成器中的非同步方法看起來更像是同步執行。不好的地方在於執行過程比較生硬。
總結
Generator 生成具有 Symbol.iterator 屬性的迭代器物件,迭代器具有 next 方法,能夠無阻塞地將程式碼掛起,下次呼叫 .next() 方法再恢復執行。
用 Generator 實現非同步程式設計只是一個 hack 用法,Generator 的語法糖 async & await 則能將非同步程式設計寫得更簡潔優雅。