跳至主要内容

[JS] Promise 的使用

const p = new Promise(callback<resolve, reject>)

// .then() 中可以放入兩個 callback,如果需要提早攔截並處理錯誤是可行的
p.then(<resolveHandler>, [<rejectHandler>])
.then(<resolveHandler2>, [<rejectHandler2>])
.catch(<rejectHandler>)
.finally(<finallyHandler>)

Promise.all('<array>')
Promise.race('<array>')

使用範例:

範例:

const p = new Promise((resolve, reject) => {
setTimeout(function () {
resolve(3);
}, 1000);
});
/**
* 方法一:在 callback 中 return new Promise
**/
p.then((value) => {
return new Promise((resolve, reject) => {
console.log(value); // 3
setTimeout(function () {
resolve(value * 2);
}, 1000);
});
});

/**
* 方法二:直接代入會 return Promise 的 function
**/
p.then(promiseFn) // 6
.then((value) => {
console.log(value); // 12
})
.catch((err) => {
// catch any error in "then"
console.log(err);
});

function promiseFn(value) {
console.log(value); // 6
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve(value * 2);
}, 1000);
});
}

目錄

[TOC]

觀念

  • Promise 有三種狀態:pending, resolved/fulfilled, rejected
  • new Promise 內的函式會立即被執行,當 resolve 得到內容後,才會執行 .then
  1. .thenresolvedCallback 中,可以得到在 new Promise 中 resolve 內所得到的值(value)。
  2. 如果在 .thenresolvedCallbackreturn 一個值,則這個值會以 Promise 物件的形式傳到下一個 .then
  3. 因此在下一個 .thenresolvedCallback 中,可以取得上一個 .then 中 return 的值。
  4. 但如果我們在 .then 中是 return 另一個 new Promise ,則下一個 .then 會等到這個 Promise 中的 resolve 得到值後才執行。
  5. 且在下一個 .thenresolvedCallback 中,可以得到上一個 new Promiseresolve 的值

基本使用

/**
* Promise 基本使用
* 在 new Promise 內的函式會被馬上執行,
* 當 resolve 得到內容後,才會執行 .then。
**/

const myPromise = new Promise((resolve, reject) => {
console.log('In new Promise, start callback'); // 立即執行
setTimeout(() => {
// 一秒後執行
let response = 10;
resolve(response);
}, 1000);
});

myPromise.then((value) => {
// 在 myPromise 被 resolve 時執行
console.log('The answer is ' + value);
});

myPromise.catch((error) => {
// 在 myPromise 被 reject 時執行
console.log('error', error);
});

Basic Promise @ JSFiddle

Promise 使用 .then 串接

/**
* Promise 使用 .then 串接
* 在 .then 裡面 resole(value) 的 value 一樣是 promise 物件,
* 可以被傳到下一個 .then 中使用。
**/

const myPromise = new Promise((resolve, reject) => {
console.log('In new Promise, start callback'); // 立即執行
setTimeout(() => {
let data = 10;
resolve(data); // 1 秒後執行
}, 1000);
});

// 在 .then 裡面在 return resolve 的 value
// 這個新的 value 會被傳到下一個 promise 的 resolver 內
myPromise
.then((value) => {
console.log('first .then'); // 被 resolve 後執行
return value + 3;
})
.then((value) => {
// 得到上一個 .then return 的值後執行
console.log('second .then');
console.log('The final value is ' + value);
});

我們可以透過 .then() 去 return 另一個 Promise,主要方法有兩種:

  1. 在 Promise 的 .then callback 中去 new 另一個 Promise
  2. 直接在 .then 中透過 function 的方式 new Promise 而非在 callback 中執行

如果是在 Promise 的 .then callback 中去 new 另一個 Promise,要把這個 Promise 前面加上 return;如果是直接把它放在 .then 的 function 中,則不用在前面加上 return。

方法一:在 .then 的 callback 中 new 另一個 Promise

我們可以在一個 Promise 的 .then 中去 return 另一個 new Promise,:

.then((value) => {
return new Promise((resolve, reject) => {
resolve(value + 3)
})
})
// 這個 .then 會等到上面那個 Promise 被 resolved 後才執行
.then((value)=>{
console.log(value)
})

範例一

/**
* 在 Promise 中 new 另一個 Promise
**/

const myPromise = new Promise((resolve, reject) => {
// 立即執行
console.log('In first Promise, start callback');
setTimeout(() => {
let data = 10;
resolve(data); // 2 秒後執行
console.log('In fist Promise, resolve data');
}, 1500);
});

// 當前面 Promise 的 resolve 得到內容後,才會執行 .then
myPromise
.then((value) => {
// 在.then 的 resolvedCallback 中,可以得到在 new Promise 中 resolve 內所得到的值(value)。
console.log('In first then, the value is ' + value);
// 如果在 .then 的 resolvedCallback 中 return 一個值,則這個值會以 Promise 物件的形式傳到下一個 .then
return value + 3;
})

.then((value) => {
// 因此在這個 .then 的 resolvedCallback 中,可以取得上一個 .then 中 return 的值。
console.log('In second then, the value is ' + value);
console.log('In second Promise, start callback');

// 如果我們在 .then 中是 return 另一個 new Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
// 2 秒後再執行
resolve(value + 3);
console.log('In second Promise, resolve data');
}, 1500);
});
})

// 則下一個 .then 會等到這個 Promise 中的 resolve 得到值後才執行。
.then((value) => {
// 在這個 .then 的 resolvedCallback 中,可以得到上一個 new Promise 中 resolve 的值
console.log('In third then, the value is ' + value);
})
.catch((reason) => {
console.log('Request Failed ' + reason);
});

output 的結果會分成兩個時間點:

--------- 立即執行
"In first Promise, start callback"
--------- 1.5 秒後執行
"In fist Promise, resolve data"
"In first then, the value is 10"
"In second then, the value is 13"
"In second Promise, start callback"
--------- 再 1.5 秒後執行
"In second Promise, resolve data"
"In third then, the value is 16"

範例二

/**
* EXAMPLE2: Return a new Promise in a Promise
**/

function waitASecond(seconds) {
console.log('start', seconds);
return new Promise((resolve, reject) => {
setTimeout(function () {
seconds++;
resolve(seconds);
}, 1000);
});
}

waitASecond(0)
.then((seconds) => {
console.log('In first .then', seconds);
return waitASecond(seconds);
})
.then((seconds) => {
console.log('In second .then', seconds);
return waitASecond(seconds);
})
.then((seconds) => {
console.log('In third .then', seconds);
});

Promise Chain: return a Promise in a Promise @ JSFiddle

方法二:在 .then 中代入會 return Promise 的函式

範例一

// EXAMPLE 1
function waitASecond(second) {
console.log(second);
return new Promise((resolve, reject) => {
setTimeout(function () {
second++;
resolve(second);
}, 1000);
});
}

waitASecond(0)
.then(waitASecond)
.then(waitASecond)
.then((second) => {
console.log(second);
});

範例二

// EXAMPLE2
const calculatePromise = new Promise((resolve, reject) => {
console.log('In new Promise, start callback');
setTimeout(() => {
let answer = 3 + 5;
resolve(answer);
}, 1000);
});

function addTwo(value) {
console.log('current value', value);
return value + 2;
}

function printResult(value) {
console.log('The final value is ' + value);
}

calculatePromise.then(addTwo).then(addTwo).then(printResult);

Use new Promise directly in .then without putting in callback @ JSFiddle

Promise 錯誤處理

可以在 Promise 的任何一個階段中加上 .catch(<err>) 做為錯誤處理,除非有特殊需要,不然 .catch() 通常會放在最後,在任何一個 .then() 的階段中有錯誤發生時,就會直接跳到最後的 .catch() 而不會繼續執行錯誤發生後的 .then()

/**
* Error Handling in Promise
**/

function waitASeconds(seconds) {
return new Promise((resolve, reject) => {
if (seconds > 3) {
reject('seconds is bigger then 3');
}
setTimeout(function () {
seconds++;
resolve(seconds);
}, 1000);
});
}

waitASeconds(0)
.then((seconds) => {
console.log('In first .then', seconds);
return waitASeconds(seconds);
})
.then((seconds) => {
console.log('In second .then', seconds);
return waitASeconds(seconds);
})
.then((seconds) => {
console.log('In third .then', seconds);
return waitASeconds(seconds);
})
.then((seconds) => {
console.log('In forth .then', seconds);
return waitASeconds(seconds);
})
.then((seconds) => {
console.log('In fifth .then', seconds);
return waitASeconds(seconds);
})
.catch((error) => {
console.warn(error);
});

Error Handling in Promise @ JSFiddle

無法被處理到的 Error(Unhandled Error)

在使用 Promise 時,有幾個需要特別留意的情況,當下面的情況發生時,這個 error 將無法被處理(handled),很有可能會看到 Uncaught (in promise)Possibly unhandled rejection

資訊

建議閱讀:

用 throw 和 reject 沒有差別的情況

JavaScript Promises - reject vs. throw @ StackOverflow

先來看一下這段程式:

function getPromise() {
const promise = new Promise((resolve, reject) => {
console.log('foo');

// 使用 throw 程式會終止在這
// throw new Error('reason');

// 使用 reject 的話程式會繼續往後執行
reject(new Error('reason'));

console.log('bar');
})
.then((data) => console.log('[getPromise]', data))
// throw 或 reject 都能被 catch 攔截
.catch((reason) => console.log('[getPromise]', reason));
}

一般來說,在 Promise 裡不論是用 throwreject 會有類似但不完全相同的效果:

  1. 相同:都會被 catch 所攔截
  2. 如果是 reject,它後面的程式會繼續被執行,所以會看到:
    • "foo", "bar", "[getPromise] Error: reason"
  3. 但如果是用 throw,它後面的程式不會繼續執行,所以會看到的是:
    • "foo", "[getPromise] Error: reason"

Uncaught Error:在 Promise 中有其他 async function 且用了 throw

然而,如果這個 Promise 裡還有其他非同步的請求,像是這樣:

  • 如果在 async function 中使用 throw,則這個 Error 將無法再被 catch 所處理,這時候就會是 Uncaught Error
  • 如果在 async function 中使用 reject,則這個 Error 仍然能改被 catch 所處理
try...catch

try...catch 只能攔截到被包在 try{...} 中「當下執行」的程式,如果 try{...} 裡面執行的是非同步的程式(scheduled code),除非能夠在這裡 await,否則攔截不到 (try...catch @ JavaScript.info)。

危險

throw 是拋出錯誤;但 reject() 比較像是一個 callback function,因此,在非同步中使用 throw 的話,即使這個 Promise 有用 catch 也無法攔截到。

function getPromise(isSuccess = true) {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('foo');

// 如果在 Promise 中的非同步的函式直接 throw
// 它的錯誤將無法在 catch 被攔截
throw new Error('reason');

// 使用 reject 的話,Error 仍然可以在 catch 中被攔截
// reject(new Error('reason'));

console.log('bar');
}, 1000);
})
.then((data) => console.log('[getPromise]', data))
.catch((reason) => {
console.log('[reason] ', reason);
});
}

同理,因為在 try{...} 中是非同步的程式,且沒有使用 await 等它執行完,所以這個錯誤是不會被 try...catch 所攔截:

try {
setTimeout(function () {
noSuchVariable; // script will die here
}, 1000);
} catch (err) {
alert("won't work");
}

最外層的 function 不是 async function

這個情況和前者有點類似:

  • p 本身是一個 async function,它可以 catch 到 sleep 這個 Promise 中 reject 的錯誤
  • main 是一個一般的 function,它「沒辦法」在 try...catch 中攔截到 p 裡面的錯誤,而是需要使用 p.catch()
async function p() {
try {
await sleep();
} catch (err) {
throw err;
}
}

function main() {
try {
p();
// 因為 main 不是 async function,這裡的 try...catch 是攔截不到 p() 裡面的 error 的
} catch (err) {
console.log(err);
}
}

Promise 沒有被 catch,且該 Promise 又沒有被 return

如果這個 Promise 本身沒有被 catch,且又沒有 return 讓其他地方處理,它會變成 Uncaught (in promise) 的錯誤:

注意

只要這個 Promise 沒有被 catch,而且又沒有 return 自己,那麼即使把這個 Promise 用 try catch 包起來也沒用。

function getPromise(isSuccess = true) {
const promise = new Promise((resolve, reject) => {
throw new Error('reason');
}).then((data) => console.log('[getPromise]', data));
// 這個 Promise 本身沒有 catch 來處理 error
// .catch((reason) => console.log('[getPromise]', reason));

// 而且沒有 return 讓其他地方處理
// return promise;
}

解決方式

要避免這樣的情況:

  1. 記得每個 Promise 都要有 catch
  2. 「或」把 Promise return 讓其他地方可以處理這個 Error

如此,這個 Promise 就可以被使用 async...awaittry...catch 或 Promise 本身的 catch 所攔截:

// 一旦 getPromise 有 return Promise,則
// 可以在 async...await 中用 try...catch 所攔截
async function main() {
try {
await getPromise(false);
// 或者使用 .catch() 亦可攔截錯誤
// .catch((reason: unknown) => {
// console.log('[main] promise catch ', reason);
// });
} catch (reason) {
console.log('[main] try catch ', reason);
}
}

進階使用

keywords: Promise.all(_Array_), Promise.race(_Array_)

這個建構式另外還提供兩種調用 promise 的方法 race() 與 all(): Promise.all(): 此方法可以同時執行大量 Promise 物件,並且在 “全部” 完成後回傳陣列。 Promise.race(): 此方法執行大量 Promise 物件,但僅會回傳最快回應的結果。

Promise.all([newPromise1, newPromise2, newPromise3, newPromise4])
.then((data) => {
// 一次性同時回傳成功訊息,回傳以上三個數值的陣列
console.log(data);
})
.catch((err) => {
// 失敗訊息 (立即)
console.log(err);
});
Promise.race([newPromise1, newPromise2, newPromise3])
.then((data) => {
// 僅會回傳一個最快完成的 resolve 或 reject
console.log('race', data);
})
.catch((err) => {
// 失敗訊息 (立即)
console.log(err);
});

此部分來自 JavaScript ES6 Promise @ 前端,沒有極限 by 卡斯伯

Demo how to use Promise.all() 和 Promise.race() @ JSFiddle

實際例子

利用 Promise 先透過 email 和 password 取得 access_key 和 secret 後,再用 access_key 和 secret 取得 token。

/**
* You can get token in a Promise
**/

const getTokenPromise = new Promise((resolve, reject) => {
request
.post(endpoint + '/users/cert')
.send({
email: '<your_email>',
password: '<your_password>',
})
.end((err, res) => {
resolve(res);
reject(err);
});
});

getTokenPromise
.then((response) => {
let parseResponse = JSON.parse(response.text);
console.log(parseResponse);
return new Promise((resolve, reject) => {
request
.post(endpoint + '/users/token')
.send({
access_key: parseResponse.access_key,
secret: parseResponse.secret,
})
.end((err, res) => {
resolve(res);
reject(err);
});
});
})
.then((response) => {
let parseResponse = JSON.parse(response.text);
// You can get Token Here
console.log(parseResponse);
})
.catch((err) => {
console.warn('getTokenPromise with error', err);
});

參考