JavaScript非同步程式設計史:回撥函式到Promise到Async/Await

Fundebug發表於2018-07-31

摘要: 非同步程式設計時JavaScript以及Node.js的一大亮點,其中有什麼心酸的黑歷史呢?

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習

回撥函式

簡單地說,回撥函式(callback function)就是給另外一個宿主函式做引數的函式。回撥函式在宿主函式內執行,執行結果返回給宿主函式。

// 給click方法做引數的匿名函式就是一個回撥函式
$("body").click(function() {
    alert(`clicked on body`);
});
複製程式碼

是不是很簡單呢?

現在,我們來實現一個回撥函式,模擬在遊戲中得分升級。

// levelOne()是宿主函式,它接收另外一個函式作為引數
// levelOne()的第二個引數callback是一個回撥函式,它的名字可以任意取,通常命名為callback只是為了易於理解
function levelOne(value, callback)
{
    var newScore = value + 5;
    callback(newScore);
}

function startGame()
{
    var currentScore = 5;
    console.log('Game Started! Current score is ' + currentScore);
    
    // levelOne()的第二個引數為回撥函式
    levelOne(currentScore, function(levelOneReturnedValue)
    {
        console.log('Level One reached! New score is ' + levelOneReturnedValue);
    });
}

startGame();
複製程式碼

執行以上程式碼,控制檯輸出是這樣的:

"Game Started! Current score is 5"
"Level One reached! New score is 10"
複製程式碼

有輸出可知,levelOne()內的程式碼(var newScore = value + 5;)執行之後,才會執行回撥函式中的程式碼(console.log('Level One reached! New score is ' + levelOneReturnedValue);)。

可知,回撥函式可以在特定程式碼執行完成之後再執行,這種執行機制在實際程式設計中非常有用。在執行一些比較耗時的程式碼時,比如讀取檔案,不需要阻塞整個程式碼去等待它完成,而可以繼續執行其他程式碼;而當檔案讀取完成後,程式碼中所繫結給檔案讀取的回撥函式會自動執行。

但是,當使用多個層級的的回撥函式時,情況會變得非常糟糕...下面是程式碼示例:

function levelOne(value, callback)
{
    var newScore = value + 5;
    callback(newScore);
}

function levelTwo(value, callback)
{
    var newScore = value + 10;
    callback(newScore);
}

function levelThree(value, callback)
{
    var newScore = value + 30;
    callback(newScore);
}

function startGame()
{
    var currentScore = 5;
    console.log('Game Started! Current score is ' + currentScore);
    levelOne(currentScore, function(levelOneReturnedValue)
    {
        console.log('Level One reached! New score is ' + levelOneReturnedValue);
        levelTwo(levelOneReturnedValue, function(levelTwoReturnedValue)
        {
            console.log('Level Two reached! New score is ' + levelTwoReturnedValue);
            levelThree(levelTwoReturnedValue, function(levelThreeReturnedValue)
            {
                console.log('Level Three reached! New score is ' + levelThreeReturnedValue);
            });
        });
    });

}

startGame();
複製程式碼

執行以上程式碼,控制檯輸出是這樣的:

"Game Started! Current score is 5"
"Level One reached! New score is 10"
"Level Two reached! New score is 20"
"Level Three reached! New score is 50"
複製程式碼

levelThree()為levelTwo()的回撥函式,而levelTwo()為levelOne()的回撥函式。那麼正確的執行順序是:levelOne() > levelTwo() > levelThree()。

如果有10個回撥函式巢狀起來呢?是不是看著就有點頭疼了!這個問題就是所謂的回撥地獄(callback hell)!有沒有解法呢?請聽下回分解!

Promise

JavaScript從**ES6(即ECMAScript 2015)**開始支援Promise。簡單地說,Promise是一個特殊的物件,它可以表示非同步操作的成功或者失敗,同時返回非同步操作的執行結果。

使用Promise建構函式來定義promise:

// 當一切正常時,呼叫resolve函式;否則呼叫reject函式
var promise = new Promise(function(resolve, reject)
{
    if ( /* everything turned out fine */ )
    {
        resolve("Stuff worked!");
    }
    else
    {
        reject(Error("It broke"));
    }
});
複製程式碼

我們將前文陷入回撥地獄的例子使用Promise改寫:

function levelOne(value)
{
    var promise, newScore = value + 5;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

function levelTwo(value)
{
    var promise, newScore = value + 10;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

function levelThree(value)
{
    var promise, newScore = value + 30;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

var startGame = new Promise(function(resolve, reject)
{
    var currentScore = 5;
    console.log('Game Started! Current score is ' + currentScore);
    resolve(currentScore);
});

// startGame返回的結果傳遞給了then函式,然後傳遞給了levelOne函式
startGame.then(levelOne)
    .then(function(result)
    {
        // result為levelOne函式的返回值
        console.log('You have reached Level One! New score is ' + result);
        return result;
    })
    .then(levelTwo)
    .then(function(result)
    {
        console.log('You have reached Level Two! New score is ' + result);
        return result;
    })
    .then(levelThree)
    .then(function(result)
    {
        console.log('You have reached Level Three! New score is ' + result);
    });
複製程式碼

執行以上程式碼,控制檯輸出還是這樣的:

"Game Started! Current score is 5"
"Level One reached! New score is 10"
"Level Two reached! New score is 20"
"Level Three reached! New score is 50"
複製程式碼

回撥函式採用了巢狀的方式依次呼叫levelOne()、levelTwo() 和levelThree(),而Promise使用then將它們連結起來。

相比回撥函式而言,Promise程式碼可讀性更高,程式碼的執行順序一目瞭然。

難道Promise就是JavaScript非同步程式設計的終點嗎?當然不是!

Async/Await

JavaScript從**ES8(即ECMAScript 2017)**開始支援Async/Await。它讓我們可以採用同步的方式呼叫Promise函式,提高非同步程式碼的可讀性。

本質上,Async/Await只是基於Promise的語法糖,它讓我們可以使用同步的方式寫非同步程式碼。但是,不要因此小看Async/Await,使用同步的方式寫非同步程式碼其實非常強大。

在定義函式時,在其前面新增一個async關鍵字,就可以在函式內使用await了。當await一個Promise時,程式碼會採用非阻塞的方式繼續執行下去。當Promise成功resolve了,await語句會正真執行結束,並獲取resolve的值。當Promise失敗reject了,await語句初會throw一個錯誤。

我們再來用async/await來改寫之前的例子:

function levelOne(value)
{
    var promise, newScore = value + 5;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

function levelTwo(value)
{
    var promise, newScore = value + 10;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

function levelThree(value)
{
    var promise, newScore = value + 30;
    return promise = new Promise(function(resolve)
    {
        resolve(newScore);
    });
}

// 只有aysnc函式內可以使用await語句
async function startGame()
{
    var currentScore = 5;
    console.log('Game Started! Current score is ' + currentScore);
    currentScore = await levelOne(currentScore);
    console.log('You have reached Level One! New score is ' + currentScore);
    currentScore = await levelTwo(currentScore);
    console.log('You have reached Level Two! New score is ' + currentScore);
    currentScore = await levelThree(currentScore);
    console.log('You have reached Level Three! New score is ' + currentScore);
}

startGame();
複製程式碼

執行以上程式碼,控制檯輸出依然是這樣的:

"Game Started! Current score is 5"
"Level One reached! New score is 10"
"Level Two reached! New score is 20"
"Level Three reached! New score is 50"
複製程式碼

忽然之間,程式碼的可讀性提高了非常多!當然,async/await的神奇之處不止於此。async/await的出錯處理非常方便,因為我們可以把同步程式碼和非同步程式碼寫在同一個try...catch...語句中。async/await程式碼除錯更加方便,使用Promise時,我們無法設定斷點,而async/await程式碼可以像同步程式碼一樣設定斷點。

參考

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了7億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!

JavaScript非同步程式設計史:回撥函式到Promise到Async/Await

版權宣告

轉載時請註明作者Fundebug以及本文地址:
blog.fundebug.com/2018/07/11/…

相關文章