淺談Generator和Promise原理及實現

Generator

熟悉ES6語法的同窗們確定對Generator(生成器)函數不陌生,這是一個化異步爲同步的利器。 栗子:javascript

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的運行規則:java

  1. JS是單線程的,只有一個主線程
  2. 函數內的代碼從上到下順序執行,遇到被調用的函數先進入被調用函數執行,待完成後繼續執行
  3. 遇到異步事件,瀏覽器另開一個線程,主線程繼續執行,待結果返回後,執行回調函數

那麼,Generator函數是如何進行異步化爲同步操做的呢? 實質上很簡單,* 和 yield 是一個標識符,在瀏覽器進行軟編譯的時候,遇到這兩個符號,自動進行了代碼轉換:git

// 異步函數
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 標識符,就會將之後的函數放入此異步函數以內,待異步返回結果後再進行執行。github

更深一步,從內存上來說:ajax

普通函數在被調用時,JS 引擎會建立一個棧幀,在裏面準備好局部變量、函數參數、臨時值、代碼執行的位置(也就是說這個函數的第一行對應到代碼區裏的第幾行機器碼),在當前棧幀裏設置好返回位置,而後將新幀壓入棧頂。待函數執行結束後,這個棧幀將被彈出棧而後銷燬,返回值會被傳給上一個棧幀。npm

當執行到 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 地址

相關文章
相關標籤/搜索