Web Development 網站開發 (10) - Javascript 非同步:Promise

Promise 是 Javascript 用來處理非同步事件的方法。如果只用 Callback 來處理非同步,當程式變得複雜並且有多層非同步事件需要處理時,程式架構會變的不好維護。Pormise 被介紹出來之後,很好的提升了非同步事件的管理與執行。除此之外也增加了程式碼的可讀性。

Callback Hell

當有很多 Callback 相互呼叫時,程式碼一層一層寫下去變得可讀性很低:

// 當有很多非同步事件要依序呼叫,可能就會長得像這樣:
setTimeout(function() {
    // do A...
    setTimeout(function() {
        // do B...
        setTimeout(function() {
            // do C...
            setTimeout(function() {
                // do D...
            }, 400)
        }, 300);
    }, 200);
}, 100);

callback hell

最容易的解決方法就是不要用匿名的 Callback,把每個 Callback Function 都定義名稱,然後從外層引用。

// 把所有 Callback 打平然後從外層呼叫,
// 當然這樣做的缺點就是你會常宣告只被用一次的 function,而且這樣的定義通常沒什麼意義

function doA() { 
    /* do A  */ 
    setTimeout(doB, 200);
}

function doB() { 
    /* do B  */ 
    setTimeout(doC, 300);
}

function doC() { 
    /* do C  */ 
    setTimeout(doD, 400);
}

function doD() { 
    /* do D  */ 
}

setTimeout(doA, 100);

Promise

Promise 在 ES6 (2015 年) 才被介紹出來,就如上所說,主要的目的就是增強 Javascript 對非同步事件的處理。 先看看以下例子,如何把上面的 setTimeout 任務改成 Promise 處理:

// 如果要把以上任務用 Promise 來實現,我們需要先把 setTimeout 包裝成 Promise
// `setTimeoutPromise` 是我們包裝後的 function, 呼叫他會回傳一個 Promise 
const setTimeoutPromise = function(ms){
    return new Promise(function(resolve) {
        setTimeout(function() {
           resolve();
        }, ms); 
    });
};

// 要使用 Promise 最常見的就是用 `then`, 雖然說 Promise 跟 Callback 的底層意義不一樣,但這邊可以先把 then 裡面的 function 想成原來作法的 call back
// Promise Chaining 的特性讓以下寫起來都是同一層的,近一步避免之前提到像 Callback Hell 一樣的問庭
setTimeoutPromise(100).then(function() {
   // do A...
   return setTimeoutPromise(200)
}).then(function() {
   // do B...
   return setTimeoutPromise(300)
}).then(function() {
   // do C...
   return setTimeoutPromise(400)
}).then(function() {
   // do D...
});
rewrite setTimeout with promise

執行一個 Promise 跟我們會用 .then ,Promise 在被呼叫時一開始會是 Pending 狀態,意思其實就是在 Event Loop 裡準備要執行的意思,一但 Call Stack 空了這個任務就會被加進去。then 裡面我們需要定義一個 Function,這個 Function 的功用其實就是 Callback,它會在非同步任務執行完後被呼叫。

// fetch 是瀏覽器內建的函式,用 fetch 可以把某個網址的內容抓下來(當然還需要考慮 CORS 的問題但這邊些不討論)。 呼叫 fetch 會回傳一個 promise,要使用他的話如以下:
fetch("https://yahoo.com").then((res) => {
    // res 這個 promise 回傳的結果
    console.log(res);
})
fetch

我們可以自己定義 Promise,基本上可以把所有各種本來是用 Callback 的非同步事件都包成 Promise。

// 要宣告一個 Promise 如以下:
new Promise(function(resolve, reject) {
    // do something.
    // 如果有結果要回傳到外面的話,我們會呼叫 resolve 並帶上要回傳的結果。
    // 如果有出錯要回傳錯誤訊息則是用 reject
});


// 在回頭看看上面用的 setTimeoutPromise, 帶入 setTimeout 所需的間隔時間 (ms)
// 回傳一個自定義的 Promise
// 我們在 setTimeout 裡呼叫了 resolve,代表了這個 promise 的完成。因為沒有要回傳東西,所以直接用了 resolve()。
const setTimeoutPromise = function(ms){
    return new Promise(function(resolve) {
        setTimeout(function() {
           resolve();
        }, ms); 
    });
};

Promise Chaining

Promise 沒有類似 Callback Hell 的原因是因為他有著 Promise Chaining 的特性。Promise 的  then 可以回傳另一個 Promise,也代表了這個 then 後面還可以接另外一個 then 去處理前一個 then 回傳的 Promise。

// 因爲我們的 then 裡面 return 了新的 Promise (setTimeoutPromise(xx))
// 所以 return 出新的 Promise 可以用下一個 then 去處理。
// .then().then().then() ... 形成了 Promise Chaining

setTimeoutPromise(100).then(function() {
   // do A...
   return setTimeoutPromise(200)
}).then(function() {
   // do B...
   return setTimeoutPromise(300)
}).then(function() {
   // do C...
   return setTimeoutPromise(400)
}).then(function() {
   // do D...
});

Promise 裡面還有很多功能,比如說 .catch 來捕捉 Promise 拋出的錯誤、Promise.all 去同時執行多個 Promise 然後匯聚結果等。更多細節可以參考

https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise

有了 Promise 以後我們以為世界變得美好,但其實不是。定義一堆 Promise 然後 then 來 then 去,一樣讓我們的程式變得很複雜。一行一行執行的程式看起來才是最直覺易懂的。

所以新的 Promise 寫法又誕生了:Async and Await... 。  

Huaying Tsai

Huaying Tsai

擅長 Python, Javascript, React, GraphQL。 想寫寫一些適合新手的程式語言教學文。 想推廣現代社會學習多元技能的風氣,建立了技能交換的平台 - https://thoth.tw
台灣