參考彭彭 - 回呼函式 Callbacks、Promises 物件、Async/Await 非同步流程控制
前言
Javascript 的主要特點有兩個:
Single Thread (單線程)
Synchronous (同步)
當主程式中遇到「非同步」的函式如排程、取資料、讀檔…等相關的函式時(如:setTimeout
、fetch
…等),主程式並不會停止並等待函式執行完成後才繼續往下跑,而是會將其放到 queue 中直到所有程式碼都跑完了,Javascript 會再回頭到 queue 中按順序將 function 拉回來處理。
1
2
3
4
5
| console.log("程式開始");
setTimeout(() => {
console.log("非同步事件");
}, 0)
console.log("程式結束");
|
程式開始
程式結束
非同步事件 <- 最後執行
上段的原始碼中,setTimeout 所定義的時間為 0,但因為是屬於非同步事件,因此還是會在其他原始碼運行完以後才執行,在 Ajax 的行為中也是一樣,當需要確保擷取到遠端資料才繼續往下執行時,如果程式碼是依序撰寫的方式,就會無法正確呈現資料,以下舉個例子示範。
舉例
以下為例,此為一個很基本的回傳n1+n2的結果的函式。
1
2
3
4
5
6
7
8
9
| function add(n1, n2) {
return n1+n2;
}
function test() {
let result = add(3, 4);
console.log(result)
}
test();
|
但若今天需求需要延遲兩秒再將結果打印出來時。
1
2
3
4
5
6
7
8
9
10
11
| function delayedAdd(n1, n2, delayTime) {
window.setTimeout(function() {
return n1+n2;
}, delayTime);
}
function test() {
let result = delayedAdd(3, 4, 2000);
console.log(result);
}
test();
|
此時會發現打印出來的 result
因為 delayedAdd
還未將結果回傳,導致直接顯示 undefined
,此時就需要來解決這種非同步流程的控制。
方法1:Callbacks
回呼函式
- 最早期使用的方式,於函式最後設定
callback
,執行需要做的事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Callback 回呼函式
function delayedAdd(n1, n2, delayTime, callback) {
// 設定排程,延遲一段時間後執行
window.setTimeout(function() {
// 延遲一段時間之後,計算加法,呼叫 callback 函式
callback(n1+n2);
}, delayTime);
}
function test() {
delayedAdd(3, 4, 2000, function(result) {
console.log(result);
});
}
test();
|
方法2:Promise
物件
- 近期於ES6提出的新方式。
- 建立 promise 物件:
new Promise(執行函式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
let p = new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 呼叫resolve將結果回傳
}, delayTime);
});
return p;
}
function test() {
let promise = delayedAdd(3, 4, 2000);
promise.then(function(result){
console.log(result);
});
}
test();
|
常見寫法,直接 return promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 呼叫resolve將結果回傳
}, delayTime);
});
}
function test() {
let promise = delayedAdd(3, 4, 2000);
promise.then(function(result){
console.log(result);
});
}
test();
|
- new Promise(resolve, reject) 的
resolve
與 reject
為 Promise 原生提供,可自行改名但順序不可異動。 resolve
對應.then()
且代表成功,而 reject
則對應到 .catch()
且代表失敗。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
reject(n1+n2); // 若使用reject,則需使用.catch
}, delayTime);
});
}
function test() {
let promise = delayedAdd(3, 4, 2000);
// 因使用 reject 回傳,這裡使用 .catch 來接
promise.catch(function(error){
console.log(error);
});
}
test();
|
方法3:Async/Await
,Promise
的語法糖
Promise
的語法糖,以便於更直觀的閱讀與撰寫非同步流程,同 Promise
於ES6提出的新玩意兒。- 注意:
await
的函式必須要確保有Promise
,否則將報錯。 - 需先宣告為
async
函式,才能使用 await
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 若使用reject,則需使用.catch
}, delayTime);
});
}
// 需先宣告為 async 函式,才可使用 await
async function test() {
let result = await delayedAdd(3, 4, 2000);
console.log(result);
}
test();
|
- 注意:使用
await
時,主程式會確實停止,等待 await
函式執行完成後,才接續往下跑,舉例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 若使用reject,則需使用.catch
}, delayTime);
});
}
// 需先宣告為 async 函式,才可使用 await
async function test() {
let result = await delayedAdd(3, 4, 2000);
console.log(result);
console.log("Hello");
}
test();
|
延伸一:多個 Promise 的資料處理:Promise.all()
- 若遇到多個
Promise
,且需將各個 Promise
的結果進行運算時,可使用 Promise.all()
來處理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 呼叫resolve將結果回傳
}, delayTime);
});
}
function test() {
let promise1 = delayedAdd(3, 4, 2000);
let promise2 = delayedAdd(2, 3, 3000);
// 多個 Promise 都完成之後,將其數值相乘
Promise.all([promise1, promise2]).then(function(results){
// 此時 results 為一個陣列 > [7, 5];
let answer = results.reduce(function(total, value) {
return total * value;
})
console.log(answer);
});
}
test();
|
延伸二:多個 Promise 使用 Async/Await
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function delayedAdd(n1, n2, delayTime) {
// 建立 Promise,使用resolve或reject回傳
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
resolve(n1+n2); // 呼叫resolve將結果回傳
}, delayTime);
});
}
function test() {
let result1 = await delayedAdd(3, 4, 2000);
let result2 = await delayedAdd(2, 3, 3000);
let answer = result1 * result2;
console.log(answer);
}
test();
|