手擼一個ES7的async函數

手擼一個ES7的async函數

本文將從js的異步歷史介紹開始,到親自手寫一個async函數的模擬實現~node


正文開始~git

js的異步歷史

咱們都知道JavaScript是單線程的,避開了操做多線程的上鎖和複雜的狀態同步問題,單線程是沒法充分利用cpu的,不過早期的JavaScript只是做爲瀏覽器的腳本工具實現的,所以採用單線程是最方便最省事的,做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?es6

水深火熱的callback

受制於單線程的緣由,JavaScript在處理異步任務的時候就出現了比較尷尬的局面,好比AJAX請求,若是事情只能一件一件的來,當用戶到服務器上去請求的時候,必須乾等到結果回來才能繼續後面的動做,這顯而易見是不能被接受的,所以callback順勢而出,到後來的nodejs崛起,實現IO讀取等異步的方式都是採用了callback的方式,也誕生了臭名昭著的callback hell,場面開始失控...github

function (param, cb){
    false.readFile(param, function(err, data){
        if(err) return cb(err);
        async1(data, function(err, data){
            if(err) return cb(err);
            async2(data, function(err, data){
                if(err) return cb(err);
                // asyncN... 不知道會有多少
            })
        })
    })
}
複製代碼

拯救callback的promise

個人意中人是蓋世英雄,有一天他會踏着七色雲彩前來拯救我ajax

es6將promise歸入了JavaScript的標準,promise表明着「承諾」,初始化了一個「承諾」以後,你只要在then中定義好這個承諾如果在將來達成要作什麼事情,在catch中定義好這個承諾失敗了須要作什麼事情,就ok了promise

const somePromiseObject = return new Promise((resolve, reject) => {
    const result = doSomeThing();
    if(result) {
        return resolve(result); // handle success
    }
    reject('err') // handle error
})

somePromiseObject
    .then(data => {
        // success callback  
    },
    err => {
        // err callback
    })
複製代碼

promise的出現完美的解救了callback的回調地獄,緣由是promise容許鏈式調用,而且能夠統一到最外層去作錯誤的catch~瀏覽器

somePromiseObject
    .then(data => {
        // do someThings..
    })
    .then(data => {
        // do someThings..
    })
    .then(data => {
        // do someThings..
    })
    .catch(err => {
        // handle errs here
    })
複製代碼

but 當咱們認爲promise就是JavaScript解決異步的最優方案的時候,ES2017 標準又引入了async 函數,使得異步操做變得更加方便bash

異步終極利器async函數

我猜中了前頭可我猜不中這結局 --promise服務器

沒錯就在es6將promise加入標準後的不久,es7又新加入一種新的異步解決方案(號稱終極解決方案)-- async函數多線程

假設有個異步函數

ajax1().then(() => {
    ajax2().then(() => {
        ajax3().then((data) => {
            console.log(data) // success
        }, err => {
            console.log(err)
        })
    },
    err => {
        console.log(err)
    })
}, err => {
    console.log(err)
})
複製代碼

儘管promise能鏈式的處理下去,可是嵌套過多的話,維護起來仍是比較頭疼的,並且錯誤的處理也至關的麻煩,須要每一個可能的錯誤都去關注,因此,咱們趕忙試試用async來實現

const asyncFun = async () => {
    try{
        await ajax1();
        await ajax2();
        const data = await ajax3();
        console.log(data) // success
    }catch(err){
        console.log(err) // handle errs here
    }
}
複製代碼

能夠看到明顯的看到,async的寫法比promise的鏈式更舒服,它容許咱們像寫同步的語法同樣去寫嵌套的異步,實際的效果也會按照async 被 await 的順序來運行,並且對開發最友好的是全部中間出現的錯誤都能被最外層的catch給捕獲,因此說不少人都認爲async將是js異步的終極解決方案,下面咱們先來認識一下實現async的函數的一個基礎函數,generator函數~

什麼是generator

  • 能夠先看下阮一峯老師的概念理解
  • generator函數最神奇的地方在於它能夠交出函數的控制權,什麼意思呢,咱們正常狀況下的函數一旦被執行就會所有執行結束,除非發生錯誤,是不能在執行中間停下來的,而generator函數就不同了,它能夠用yield關鍵字來將函數的執行暫停,generator函數執行後會返回一個迭代器(也能夠理解爲指針),這個指針對象擁有一個關鍵的next方法,用來移動當前指針所在的位置,每次調用next返回一個對象,包含2個內容:value:當前generator函數中yield語句後面表達式的值,done:bool值,標識當前迭代器是否結束迭代了,也就是說是否全部的yield語句都走完了;
  • genarator 不只僅能夠暫停一個函數的執行,還能夠在執行的時候送入數據給函數,改變函數中yield關鍵字後面表達式的值;
function* gen(x) {
        const y = yield x + 2;
        const y1 = yield y + 3;
        return y1;
    }
    
    var g = gen(1);
    console.log(g.next()); // { value: 3, done: false }
    console.log(g.next(2)); // { value: 5, done: false }
    console.log(g.next()); // { value: undefined, done: true }
複製代碼
  • 能夠看到運行g.next(2)以前y的值已是3了,+3應該是6,實際返回的確實5,由於咱們給入的值會替換掉yield關鍵字後面的表達式,也就是y=2了,因此next(2)後的值是5而不是6;

既然generator能夠暫停,若是咱們每次遇到promise就暫停,等拿到promise.then()執行的結果在返回data,豈不是就是async函數的表現形式?dei,這確實就是async實現的基本原理,下面咱們一塊兒來手擼一個模擬async的函數--"myAsync",盤它!

盤它

1. 根據async函數的表現,async關鍵字後面跟的是一個generator函數,咱們用函數來模擬,以下結果:

// 函數A(正常狀況);
const test = async function myGenerator(){
    const data = await Promise.resolve("success");
    console.log(data);
}

// 函數B(模擬狀況);
function myAsync(myGenerator) {
    // handle...
}
function* myGenerator() {
    const data = yield Promise.resolve("success");
    console.log(data); // success
}
const test = myAsync(myGenerator);
複製代碼

2. ok,而後咱們來實現myAsync函數,這個函數會接受一個generator函數,咱們知道generator函數調用後纔會生成迭代器,拿到迭代器後,咱們確定須要調用next()來獲取下一個yield的值,而後若是這個值是一個promise,那咱們就調用then拿到結果後再next到下一次的迭代中,不然,咱們直接把拿到的數據不作處理直接給到next--核心思想:generator的迭代器結果,是promise就等異步完成,不然就直接返回數據,而後遞歸調用handle函數處理下一個迭代,直到迭代器的done是true,返回,看代碼:

// 函數B(模擬狀況);
function myAsync(myGenerator) {
    const gen = myGenerator(); // 生成迭代器
    const handle = genResult => {
        if (genResult.done) return; // 若是迭代器結束了,直接返回;
        return genResult.value instanceof Promise // 判斷當前迭代器的value是不是Promise的實例
            ? genResult.value
                  // 若是是,則等待異步完成後繼續遞歸下一個迭代,並把resolve後的data帶過去
                  .then(data => handle(gen.next(data)))
                  .catch(err => gen.throw(err)) // gen.throw 能夠拋出一個容許外層去catch的err
            : handle(gen.next(genResult.value)); // 若是不是promise,就能夠直接遞歸下一次迭代了
    };
    try {
        handle(gen.next()); // 開始處理next迭代
    } catch (err) {
        throw err;
    }
}
function* myGenerator() {
    const data = yield Promise.resolve("success");
    console.log(data); // success
}
const test = myAsync(myGenerator);
複製代碼

到如今其實咱們的核心功能handle函數就完成了,如今去調用next()方法,已經看到能夠打印success了,可是,除了正常的promise,咱們還要考慮下面的狀況

// 函數A(正常狀況);
const a = {
    then: () => {
        console.log("then");
        return 123123;
    }
};

const test0 = async function() {};

const test1 = async function() {
    return 123;
};
const test2 = async function() {
    console.log(123);
};
const test4 = async function() {
    return a;
};

test0().then(console.log); // undefined
test1().then(console.log); // 123
test2().then(console.log); // 123 undefined
test4().then(console.log); // then
複製代碼

也就是說在async接受到的函數不是generator函數的狀況下:

  1. async函數默認返回一Promise.resolve(undefined)
  2. 當async接受到的函數有返回值,而且返回值不是promise的狀況下,async函數默認用promise包裝這個返回結果
  3. 當async接受到函數沒有返回值,async會直接運行函數,並返回一個Promise.resolve(undefined)
  4. 考慮到thenable這種promise的鴨子類型函數的特殊性,我嘗試去返回了一個這種類型的對象,果真,發現then被執行了~

2.接着,咱們來完善邊界狀況的處理

因此如今的代碼變成

// 函數B(模擬狀況);
function myAsync(myGenerator) {
    // 判斷接受到的參數不是一個generator函數
    if (
        Object.prototype.toString.call(myGenerator) !==
        "[object GeneratorFunction]"
    ) {
        // 若是是一個普通函數
        if (
            Object.prototype.toString.call(myGenerator) === "[object Function]"
        ) {
            return new Promise((resolve, reject) => {
                // 默認返回一個promise對象
                try {
                    const data = myGenerator();
                    return resolve(data); // 嘗試運行這個函數,並把結果resolve出去
                } catch (err) {
                    return reject(err); // 失敗處理
                }
            });
        }
        // 若是參數含有then這個方法--thenable 鴨子類型
        if (typeof myGenerator.then === "function") {
            return new Promise((resolve, reject) => {
                try {
                    // 運行這個對象的then函數,並resolve出去
                    const data = myGenerator.then();
                    return resolve(data);
                } catch (err) {
                    return reject(err); // 失敗處理
                }
            });
        }
        // 剩下的狀況,統一resolve出去給的參數
        return Promise.resolve(myGenerator);
    }
    const gen = myGenerator(); // 生成迭代器
    const handle = genResult => {
        if (genResult.done) return; // 若是迭代器結束了,直接返回;
        return genResult.value instanceof Promise // 判斷當前迭代器的value是不是Promise的實例
            ? genResult.value
                  .then(data => handle(gen.next(data))) // 若是是,則等待異步完成後繼續遞歸下一個迭代,並把resolve後的data帶過去
                  .catch(err => gen.throw(err)) // gen.throw 能夠拋出一個容許外層去catch的err
            : handle(gen.next(genResult.value)); // 若是不是promise,就能夠直接遞歸下一次迭代了
    };
    try {
        handle(gen.next()); // 開始處理next迭代
    } catch (err) {
        throw err;
    }
}

const a = {
    then: () => {
        console.log("then");
        return 123123;
    }
};

const test0 = myAsync(function() {});

const test1 = myAsync(function() {
    return 123;
});

const test2 = myAsync(function() {
    console.log(123);
});
const test4 = myAsync(function() {
    return a;
});

test0.then(console.log); // undefined
test1.then(console.log); // 123
test2.then(console.log); // 123 undefined
test4.then(console.log); // then
複製代碼

有的同窗可能會說,咦,es7的 async 函數返回的那個是個函數,須要執行才能拿到結果,好比test0 應該是 test() 返回的是promise,可是模擬出來的怎麼直接就拿到返回的promise了,其實很簡單,你只要再用一個函數(好比myAsyncWrapper)把這個函數包裝一下就ok~

4.到此,咱們的模擬就結束了,如今讓咱們來盡情的實驗一下~

myAsync(function*() {
    try {
        const data1 = yield new Promise(res => {
            setTimeout(() => {
                res(1234);
                console.log("step 1"); // step 1
            }, 1000);
        });
        console.log(data1); // step 1 打印1s後 打印 123
        const data2 = yield new Promise(res => {
            setTimeout(() => {
                res(12342);
                console.log("step 2"); // step 2
            }, 1000);
        });
        console.log(data2); // step 2 打印1s後打印 12342
    } catch (err) {
        console.log(888, err); 
    }
});
複製代碼

完美啊有麼有,再來試試reject

myAsync(function*() {
    try {
        yield Promise.reject(123);
        const data1 = yield new Promise(res => {
            setTimeout(() => {
                res(1234);
                console.log("step 1");
            }, 1000);
        });
        console.log(data1);
        const data2 = yield new Promise(res => {
            setTimeout(() => {
                res(12342);
                console.log("step 2");
            }, 1000);
        });
        console.log(data2);
    } catch (err) {
        console.log(888, err); // 888 123
    }
});
複製代碼

沒問題直接輸出了888 123

5. 完美結束

總結

1.async 函數是generator+promise的語法糖,可能es7具體的實現必定有必定的優化和處理,可是我的以爲理論上誤差不大

2.想要用的駕輕就熟仍是建議去猜想和理解一下實現的原理

3.特此聲明:以上內容基本都是模擬實現,並非真實的實現原理,沒有寫測試,所以可能有漏洞的存在,歡迎各位大佬批評指正,但願你們可以共同進步~

代碼demo傳送門

相關文章
相關標籤/搜索