聊聊Generator在實際開發中的使用

一直都對生成器似懂非懂的感受,知道生成器的特色:數組

  • 能夠在執行中暫停
  • 執行生成器會返回迭代器

可是一直不明白生成器在實際開發的做用,下面一塊兒來挖掘其能夠解決哪些開發痛點。bash

先熟悉下迭代器

迭代器最經常使用的場景應該是for...of語法了:異步

for (let v of [1, 2, 3]) {
    console.log(v);
}
複製代碼

這裏咱們用for...of語法去遍歷數組每一項的值,數組之因此能夠被for...of識別,是由於數組實現了可迭代協議:async

一個對象必須實現[Symbol.iterator]方法,其返回一個符合迭代器協議的對象函數

什麼是迭代器協議?迭代器協議要求一個對象實現next方法,其返回一個包含兩個屬性的對象:測試

  • done: true/false,只是有沒有超過可迭代次數
  • value: 任何JavaScript值,done爲true時能夠省略

基於可迭代對象協議,咱們也能夠實現一個可迭代對象: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中進行。基於這種方式,能夠很好地將遍歷與處理的代碼分隔開。

生成器與Promise結合

生成器在執行過程當中是能夠暫時掛起的,而且掛起狀態是不會阻塞主線程的,後續能夠用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的語法糖。

結束

其實,平時開發中一直都在用生成器,只是沒注意到而已。

相關文章
相關標籤/搜索