首先,在 Day4 的時候有提到 JS 是單執行緒的,也就是在 JS 引擎中,編譯並執行 JS 在同時間內只能執行一件任務。
為什麼要設計成單執行緒?這有些原因,在瀏覽器裡面其實並不只有執行 JS 這件事情要做而已,而且試想如果 JS 可以同時處理多個任務,那在操作修改 DOM、執行 JS 動畫、執行非同步任務等這些事情都混在一起處理,會增加複雜性,還要處理一些併發的問題。
但單執行緒如果碰到一個執行時間較久的任務,會導致後面的任務一直排隊等待,如果此任務和頁面渲染有關,就會導致頁面卡死,為了解決這個問題,JS 將任務的執行類型分成同步和非同步。
而處理非同步任務的方式有好幾種,包括 Callback、Promise、async/await、Generator,以 Callback function 來說,會有 callback hell 造成程式碼可讀性降低的問題,並且假設使用第三方 API 時用到了 callback 去執行下一步的任務,有可能會因為第三方 API 的設計導致你的 callback function 被呼叫多次、沒有被呼叫,又是有時同步/非同步執行,要避免這些問題都需要額外的判斷程式碼。
而 Promise 解決了上述的問題,所以這篇來了解一下 Prmoise。
第一個是 Promise 的核心特性是具有內部狀態,這些狀態決定了非同步操作的結果:
若 Promise 已經不處於 pending 狀態,已經是 fulfilled 或是 rejected 狀態,就統稱為 settled 狀態,此狀態不可再變更。
第二個是用到了觀察者模式的觀念,使用 .then()
或 .catch()
來訂閱 Promise 的狀態變化(Fulfilled 或 Rejected),一旦狀態從 pending 變為 settled,Promise 內部會通知所有觀察者,執行他們的 callback function。
接下來看個範例來了解 Promise 語法:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hi');
}, 1000);
// 執行一些非同步作業,最終呼叫:
// resolve(someValue); // 實現
// reject("failure reason"); // 拒絕
});
myPromise.then((value) => {
// 在 myPromise 被 resolve 時執行
console.log(value); // hi
});
myPromise.catch((error) => {
// 在 myPromise 被 reject 時執行
console.log(error);
});
在範例中,我們透過 new 關鍵字從 Promise 類別產生一個 Promise 物件,並且建立 Promise 物件時傳入一個函式當作參數,我們在這裡先叫它為 executor,executor 函式會帶入兩個函式 resolve & reject 作為參數。
在宣告 new Promise
時,executor 會自動執行,若是成功完成後會執行 resolve 函式,如果有錯誤則執行 reject 函式。
new Promise((resolve, reject) => {});
範例中,myPromise 為一個 Promise 物件,因為物件內的函式有成功完成執行,resolve 函式的參數值傳給 .then()
當作回呼函式的參數,並印出 hi,進入到 resolved 的狀態。
順帶一提,在任何一個 .then()
的回呼函式中有錯誤發生時,就會直接跳到最後的 .catch()
而不會繼續執行。
目前 JS 沒有提供專門的函式或功能去取消 Promise 的執行,是一個使用 Promise 上的限制。
不過根據 Promise - is it possible to force cancel a promise
這個在 Stackoverflow 問答的內容,知道我們可以使用一些第三方套件像是 bluebird、cancelable-promise 和 Web API AbortController 去做處理。
不過這些套件我都還沒使用過,等未來有實際使用後更了解再來跟讀者們分享,有興趣的讀者可以先自己研究一下XD。
.then()
這個函式接收兩個參數,Promise 在成功及失敗情況時的回呼函式,如果接收的參數不是函式則直接返回該值。
p.then(function(value) {
// fulfillment
}, function(reason) {
// rejection
});
Promise fulfilled 時被呼叫。
Promise rejected 時被呼叫。
回傳的是 pending 狀態的 Promise 物件或是同步的某種資料型態的值,不過非 Promise 物件的值會被轉成 Promise 物件。
這個函式用來處理 Promise 的 rejected 狀態,等同於呼叫 Promise.prototype.then(undefined, onRejected)
。
catch() 回傳值也是一個 Promise。
p.catch(function(reason) {
// rejection
});
catch()
唯一的參數,Promise rejected 時被呼叫。
從 Promise 原始碼 可以知道 Promise 內部有透過 try catch 去做錯誤處理。
簡化後擷取片段程式碼的樣子大概長這樣:
class Promise {
constructor(cb) {
try {
cb(this.onResolve, this.onReject);
} catch (e) {
this.#onFail(e);
}
}
// ...略
}
所以這兩段程式碼的作用其實是一樣的:
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
// 等同於:
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
那麼看完前面的介紹,有個小問題,catch()
內的 console.log
會執行嗎?裡面有非同步的 setTimeout 喔~
new Promise(function(resolve, reject) {
setTimeout(() => {
throw new Error("Whoops!");
}, 2000);
}).catch(error => console.log(error)); // 會執行嗎?
當然是不會執行,等 new Promise()
內部的函式執行完後,非同步的 setTimeout 才開始執行。
至於怎麼處理 setTimeout 拋出的錯誤呢 ? 這個 Stackoverflow 問題-How to handle errors from setTimeout in JavaScript? 的討論我覺得還不錯,有興趣的讀者可以進去閱讀看看,像改寫成這樣就能處理到了。
new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 300);
})
這個方法會回傳一個 Promise 物件,並且在 Promise 無論是 resolve 或是 rejected 都會執行它的回呼函式,有兩個特性要注意:
finally()
的回呼函式不會帶入參數。finally()
return 東西的話將會被忽略。它常放在最後做一些清理的任務,像是變更 loading 狀態、關閉連線,這裡借用 MDN 的範例,透過 finally()
將 loading 狀態做變更。
let isLoading = true;
fetch(myRequest).then(function(response) {
const contentType = response.headers.get("content-type");
if(contentType && contentType.includes("application/json")) {
return response.json();
}
throw new TypeError("Oops, we haven't got JSON!");
})
.then(function(json) { /* process your JSON further */ })
.catch(function(error) { console.log(error); })
.finally(function() { isLoading = false; });
我們在前面的段落介紹了 .then()
會回傳 Promise 物件,而該物件可以加入到 .then()
的第一個參數函式 onFulfilled,達成鏈式寫法,例如以下範例就是透過鏈式寫法依序執行多個 Promise。
function returnPromise(time) {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve(`${time} seconds done`);
}, time * 1000);
});
}
const randomTime = Math.floor(Math.random() * 5);
returnPromise(randomTime).then((promise1value) => {
console.log('myPromise1 resolve:', promise1value);
return returnPromise(2); // then 函式回傳 Promise 物件
}).then((promise2value) => {
console.log('myPromise2 resolve:', promise2value);
return returnPromise(3);
}).then((promise3value) => {
console.log('myPromise3 resolve:', promise3value);
}).catch((error) => {
console.log('error');
});
執行結果:
這樣的寫法能夠解決傳統 callback 的 callback hell 的問題,程式碼也更好閱讀,也能容易的做錯誤處理。
最後做個補充,我們可以把 Callback 轉成 Promise 的寫法,像 Node.js 就提供了 util.promisify() 這個 API,能將原本是 Callback 轉成 Promise 的寫法:
const fs = require('fs');
const util = require('util');
fs.readFile('./index.js', 'utf8', (err, text) => {
if (err) {
console.log('Error', err);
} else {
console.log(text);
}
});
// 轉換
const readFile = util.promisify(fs.readFile);
readFile('./index.js', 'utf8')
.then((text) => {
console.log(text);
})
.catch((err) => {
console.log('Error', err);
});
當然也可以自己實作一個 promisify 函式,以下拆分成幾個步驟紀錄如下:
promisify 會接受一個函式並把它轉為非同步,並回傳一個函式,所以初步寫的結構如下:
const promisify = (fn) => {
return function (...args) {
}
}
接著我們知道這個回傳的函式會是非同步的,所以我們讓它回傳一個 Promise,後面就可以接 .then()
去做後續處理。
const promisify = (fn) => {
return function (...args) {
}
}
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
接著要處理 Promise resolve & reject 的部分,早期還沒有 Promise、async/await 時使用的是 callback function 處理非同步,所以在 Promise 內定義一個 callback function。
const promisify = (fn) => {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) {
if (err) {
return reject(err);
}
return resolve(results.length === 1 ? results[0] : results);
}
args.push(callback);
fn.call(this, ...args);
});
};
};
最後將 callback function 傳入被 promisify 的函式即可。
Promise 類別還實作了其他的靜態方法,像 Promise.all()
、Promise.race()
,將於下篇文章介紹它們的用法以及如何實作這些函式。
Master the JavaScript Interview: What is a Promise?
Promise - is it possible to force cancel a promise
JavaScript Promise Chain - The art of handling promises
How to Write Your Own Promisify Function from Scratch
從 Promise chain 中拋出錯誤 範例
fetch('/api/newsletter', {
method: 'POST',
body: JSON.stringify({ email: enteredEmail }),
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
return response.json().then((data) => {
throw new Error(data.message || 'Something went wrong!'); // 進入 catch block
});
})
.then((data) => {
notificationCtx.showNotification({
title: 'Success!',
message: 'Successfully registered for newsletter!',
status: 'success',
});
})
.catch((error) => {
notificationCtx.showNotification({
title: 'Error!',
message: error.message || 'Something went wrong!',
status: 'error',
});
});
}