淺談Generator和Promise原理及實現

鬼鬼鬼發表於2018-08-07

Generator

熟悉ES6語法的同學們肯定對Generator(生成器)函式不陌生,這是一個化非同步為同步的利器。 栗子:

function* abc() {
    let count = 0;
    while(true) {
        let msg = yield ++count;
        console.log(msg);
    }
}

let iter = abc();
console.log(iter.next().value);
// 1
console.log(iter.next('abc').value);
// 'abc'
// 2
複製程式碼

首先,我們先簡單回顧一下JS的執行規則:

  1. JS是單執行緒的,只有一個主執行緒
  2. 函式內的程式碼從上到下順序執行,遇到被呼叫的函式先進入被呼叫函式執行,待完成後繼續執行
  3. 遇到非同步事件,瀏覽器另開一個執行緒,主執行緒繼續執行,待結果返回後,執行回撥函式

那麼,Generator函式是如何進行非同步化為同步操作的呢? 實質上很簡單,* 和 yield 是一個識別符號,在瀏覽器進行軟編譯的時候,遇到這兩個符號,自動進行了程式碼轉換:

// 非同步函式
function asy() {
    $.ajax({
        url: 'test.txt',
        dataType: 'text',
        success() {
            console.log("我是非同步程式碼");
        }
    })
}

function* gener() {
    let asy = yield asy();
    yield console.log("我是同步程式碼");
}
let it = gener().next();
it.then(function() {
    it.next();
})
// 我是非同步程式碼
// 我是同步程式碼
複製程式碼
// 瀏覽器編譯之後
function gener() {
    // let asy = yield asy(); 替換為
    $.ajax({
        url: 'test.txt',
        dataType: 'text',
        success() {
            console.log("我是非同步程式碼");
            // next 之後執行以下
            console.log("我是同步程式碼");
        }
    })
    // yield console.log("我是同步程式碼");
}
複製程式碼

整個過程類似於,瀏覽器遇到識別符號 * 之後,就明白這個函式是生成器函式,一旦遇到 yield 識別符號,就會將以後的函式放入此非同步函式之內,待非同步返回結果後再進行執行。

更深一步,從記憶體上來講:

普通函式在被呼叫時,JS 引擎會建立一個棧幀,在裡面準備好區域性變數、函式引數、臨時值、程式碼執行的位置(也就是說這個函式的第一行對應到程式碼區裡的第幾行機器碼),在當前棧幀裡設定好返回位置,然後將新幀壓入棧頂。待函式執行結束後,這個棧幀將被彈出棧然後銷燬,返回值會被傳給上一個棧幀。

當執行到 yield 語句時,Generator 的棧幀同樣會被彈出棧外,但Generator在這裡耍了個花招——它在堆裡儲存了棧幀的引用(或拷貝)!這樣當 it.next 方法被呼叫時,JS引擎便不會重新建立一個棧幀,而是把堆裡的棧幀直接入棧。因為棧幀裡儲存了函式執行所需的全部上下文以及當前執行的位置,所以當這一切都被恢復如初之時,就好像程式從原本暫停的地方繼續向前執行了。

而因為每次 yield 和 it.next 都對應一次出棧和入棧,所以可以直接利用已有的棧機制,實現值的傳出和傳入。

至此,Generator 的魔力已經揭開。

Promise

Promise的用法大家應該都很熟悉:

let pr = new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve("成功執行啦");
    }, 2000)
})
pr.then(function(data) {
    console.log(data); // 成功執行啦
})
複製程式碼

那麼 Promise 是如何實現非同步載入的呢?

Promise 並沒有大家想的那麼神祕,其本質就是一個狀態機。

想要實現一個土生土長的 Promise 其實很簡單,狀態機,我們需要幾個引數:

  • __success_res 用來儲存成功時的引數
  • __error_res 用來儲存失敗時的引數
  • __status 用來儲存狀態
  • __watchList 用來儲存執行佇列

下面就手動實現一個 Promise

class Promise1 {
    constructor(fn) {
        // 執行佇列
        this.__watchList = [];
        // 成功結果
        this.__success_res = null;
        // 失敗結果
        this.__error_res = null;
        // 狀態
        this.__status = "";
        fn((...args) => {
            // 儲存成功資料
            this.__success_res = args;
            // 狀態改為成功
            this.__status = "success";
            // 若為非同步則回頭執行then成功方法
            this.__watchList.forEach(element => {
                element.fn1(...args);
            });
        }, (...args) => {
            // 儲存失敗資料
            this.__error_res = args;
            // 狀態改為失敗
            this.__status = "error";
            // 若為非同步則回頭執行then失敗方法
            this.__watchList.forEach(element => {
                element.fn2(...args);
            });
        });
    }

    // then 函式
    then(fn1, fn2) {
        if (this.__status === "success") {
            fn1(...this.__success_res);
        } else if (this.__status === "error") {
            fn2(...this.__error_res);
        } else {
            this.__watchList.push({
                fn1,
                fn2
            })
        }
    }
}
複製程式碼

這樣就簡單實現了 Promise 的功能,在使用上和JS的 Promise 並無其他區別,若想實現 Promise.all 方法,則只需要進行小小的迭代:

Promise1.all = function(arr) {
    // 存放結果集
    let result = [];
    return Promise1(function(resolve, reject) {
        let i = 0;
        // 進行迭代執行
        function next() {
            arr[i].then(function(res) {
                // 存放每個方法的返回值
                result.push(res);
                i++;
                // 若全部執行完
                if (i === result.length) {
                    // 執行then回撥
                    resolve(result);
                } else {
                    // 繼續迭代
                    next();
                }
            }, reject)
        }
    })
}
複製程式碼

至此,Generator 和 Promise 都已解析完成。

最後不好意思推廣一下我基於 Taro 框架寫的元件庫:MP-ColorUI

可以順手 star 一下我就很開心啦,謝謝大家。

點這裡是文件

點這裡是 GitHUb 地址

淺談Generator和Promise原理及實現

相關文章