一直都對生成器似懂非懂的感受,知道生成器的特色:數組
可是一直不明白生成器在實際開發的做用,下面一塊兒來挖掘其能夠解決哪些開發痛點。bash
迭代器最經常使用的場景應該是for...of語法了:異步
for (let v of [1, 2, 3]) {
console.log(v);
}
複製代碼
這裏咱們用for...of語法去遍歷數組每一項的值,數組之因此能夠被for...of識別,是由於數組實現了可迭代協議:async
一個對象必須實現[Symbol.iterator]方法,其返回一個符合迭代器協議的對象函數
什麼是迭代器協議?迭代器協議要求一個對象實現next方法,其返回一個包含兩個屬性的對象:測試
基於可迭代對象協議,咱們也能夠實現一個可迭代對象:ui
const obj = {
[Symbol.iterator]() {
const MAX_COUNT = 5;
let count = 0;
return {
next() {
return {
value: ++count,
done: count === MAX_COUNT
};
}
};
}
};
複製代碼
而後使用for...of遍歷這個可迭代對象:spa
for (let v of obj) {
console.log(v);
}
複製代碼
結果打印出:1 2 3 4,既然明白了for..of的本質其實就是對可迭代對象的封裝,那麼咱們也能夠實現一個for...of:線程
function forOf(obj, callback) {
const iterator = obj[Symbol.iterator]();
let { value, done } = iterator.next();
while (!done) {
callback(value);
const o = iterator.next();
value = o.value;
done = o.done;
}
}
複製代碼
嘗試使用下這個'for...of':code
forOf([1, 2, 3], v => {
console.log(v);
});
複製代碼
結果正常打印出:1 2 3
回顧forOf的實現,也就是說for...of語法實際上是一個迭代器的語法糖,咱們用回調的形式實現了for..of,那麼反過來,是否是能夠用for...of來簡化回調呢?
好比說,對於一個樹形結構:
const treeData = {
value: 1,
children: [
{
value: 2,
children: null
},
{
value: 3,
children: [
{
value: 4,
children: null
}
]
}
]
};
複製代碼
想要去遍歷這個樹形結構,正常的想法就是去遞歸:
function traverse(tree, callback) {
callback(tree.value);
if (tree.children) {
for (let child of children) {
traverse(child, callback);
}
}
}
traverse(treeData, v => {
console.log(v);
});
複製代碼
能夠看到對於樹結點的處理是經過回調完成的,是與遍歷操做耦合在一塊兒的,咱們能夠嘗試用迭代器的方式去處理:
function* traverse(tree) {
yield tree.value;
if (tree.children) {
for (let child of tree.children) {
yield* traverse(child);
}
}
}
const iterator = traverse(treeData);
for (let v of iterator) {
console.log(v);
}
複製代碼
咱們定義了一個生成器函數去遍歷樹結點,只是遍歷,並無包含其它操做。執行生成器函數會返回一個迭代器,在生成器內部是能夠控制每次迭代的值的,而後迭代器是能夠使用for...of語法的,對於結點的處理就放在for...of中進行。基於這種方式,能夠很好地將遍歷與處理的代碼分隔開。
生成器在執行過程當中是能夠暫時掛起的,而且掛起狀態是不會阻塞主線程的,後續能夠用next方法讓其被喚醒繼續執行。Promise能夠在將來觸發某種條件的狀況下獲得事先承諾的值,自己也就是用來處理異步任務的,將其與生成器結合,能夠更優雅地去處理異步任務。
function _async(generator) {
const iterator = generator();
function handle(iteratorResult) {
const { value, done } = iteratorResult;
if (done) {
return;
}
if (value instanceof Promise) {
value.then(v => handle(iterator.next(v)))
.catch(error => iterator.throw(error));
}
}
try {
handle(iterator.next());
}
catch (error) {
iterator.throw(error);
}
}
複製代碼
_async函數接收一個生成器函數做爲參數,在內部首先執行生成器函數返回迭代器iterator,而後執行內部函數handle,其參數爲迭代器調用next方法的結果,並用try...catch捕獲異常。咱們知道,迭代器的next方法會喚醒生成器繼續執行,並在下一個yield關鍵字處從新掛起,那麼此處就很是適合等待異步任務的結果。
next方法的結果是一個對象,包含兩個屬性value與done,當done爲true時,表示迭代完成,也就退出handle函數。不然,判斷value是否是一個Promise,爲何呢?由於須要知道生成器再次被喚醒的時機,這個時機就是異步任務完成的時候,而Promise能很好地知足這個要求。Promise有三個狀態:pending、fulfilled、rejected,當從pending變成fulfilled狀態時,Promise會執行then方法註冊的回調,參數爲fulfilled的值。
這裏,fulfilled狀態就是異步任務完成的標記,而fulfilled的值就是異步任務的結果。因此這裏,咱們then方法的註冊回調爲:繼續執行handle函數去處理下一個迭代值,也就是下一個異步任務,若是還有的話。
固然,異步任務失敗,狀態就從pending到rejected了,回去執行catch方法註冊的回調,本質都是同樣的。
咱們模擬幾個異步任務來測試下:
function getAsyncData(wait) {
return new Promise(resolve => {
setTimeout(() => {
resolve(wait);
}, wait * 1000);
});
}
_async(function* () {
const result1 = yield getAsyncData(1);
console.log(result1);
const result2 = yield getAsyncData(2);
console.log(result2);
const result3 = yield getAsyncData(3);
console.log(result3);
});
複製代碼
測試結果爲:在第1s、3s、6s的時候打印了1 2 3,符合咱們的預期。
其實,這就是咱們平時經常使用的async...await語法的原理,即async函數就是生成器加Promise的語法糖。
其實,平時開發中一直都在用生成器,只是沒注意到而已。