JavaScript(6):深入理解Promise的原理及實際應用

Blog背景圖

Promise為了解決什麼問題

  • JavaScript Promise是一種用來處理異步操作的對象,它代表一個異步操作的成功(resolve)或失敗(reject)的結果。(以下皆使用setTimeout()示範異步操作的案例)
  • 透過這種方法能避免回調地獄(Callback Hell)的問題。
  • 以下直接透過Callback Hell的函數說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 假設有一個需求,我們需要連續執行三個異步操作:A、B、C。
function operationA(callbackA) {
setTimeout(() => {
console.log("Operation A completed");
callbackA();
}, 1000); //← 等待1秒執行
}
function operationB(callbackB) {
setTimeout(() => {
console.log("Operation B completed");
callbackB();
}, 1500); //← 等待1.5秒執行
}
function operationC(callbackC) {
setTimeout(() => {
console.log("Operation C completed");
callbackC();
}, 2000); //← 等待2秒執行
}
// Callback Hell
operationA(() => {
operationB(() => {
operationC(() => {
console.log("All operations completed");
});
});
});
// 在這個例子中,operationA 完成後,我們才能開始 operationB,然後是 operationC。這種嵌套結構會使得代碼難以閱讀,特別是當有更多異步操作時,這種形式會變得更加複雜和難以管理,這就是所謂的 Callback Hell。

顯示如下(依序出現A->B->C->All):

1
2
3
4
Operation A completed
Operation B completed
Operation C completed
All operations completed

這邊特別強調多層次結構的Callback Hell會變成像是波動拳的結構!!!難以維護
Callback Hell示意圖


  • Promise的寫法能夠避免內嵌的回調函數寫法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function operationA() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Operation A completed");
resolve();
}, 1000); //← 等待1秒執行
});
}
function operationB() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Operation B completed");
resolve();
}, 1500); //← 等待1.5秒執行
});
}
function operationC() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Operation C completed");
resolve();
}, 2000);
});
}
// 使用 Promise 的方式
operationA()
.then(() => operationB())
.then(() => operationC())
.then(() => console.log("All operations completed"))
.catch((error) => console.error("Error:", error));

顯示如下(依序出現A->B->C->All)

1
2
3
4
Operation A completed
Operation B completed
Operation C completed
All operations completed

Promise狀態

  • 當你使用 Promise時,它可以處於三種不同的狀態(狀態改變後就不可逆):
    • Peding(等待中)
      • 創建使用的初始狀態,可在內部放置異步事件
    • Fulfiled(已完成)
      • 異步事件完成時,透過resolve()方法將Promise狀態改變至Fulfiled(已完成)
      • 此時可以透過.then回傳Promise執行的結果
    • Rejected(已拒絕)
      • 異步事件失敗時,透過reject()方法將Promise狀態改變至Rejected(已失敗)
      • 此時可透過.catch處理Promise的錯誤訊息
  • .finally()—最終結果
    • 不論完成或是被拒絕均會執行的回調函數

  • 基本使用概念如下

Promise流程圖

1
2
3
4
5
6
7
8
9
10
operationA()
.then((res) => {
console.log("操作成功:", res);
})
.catch((error) => {
console.error("錯誤:", error);
})
.finally(() => {
console.log("不論成功還是失敗都會執行的代碼");
});

  • Fulfilled案例(如果Nb小於10代表成功)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function operationA(Number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Number < 10) {
resolve({ Status: "OK" });
} else {
reject({ Status: "Error" });
}
}, 1000); //← 等待1秒執行
});
}
// 使用 Promise 的方式
const Nb = 5
operationA(Nb)
.then((res) => {
console.log("異步事件-正確執行回傳資料", res)
})
.catch((error) => {
console.error("異步事件-異常訊息:", error)
})

顯示如下(.then()捕獲異步事件回傳資料):

1
異步事件-正確執行回傳資料 { Status: 'OK' }

  • reject案例(如果Nb大於10代表成功):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function operationA(Number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Number < 10) {
resolve({ Status: "OK" });
} else {
reject({ Status: "Error" });
}
}, 1000);
});
}
// 使用 Promise 的方式
const Nb = 20
operationA(Nb)
.then((res) => {
console.log("異步事件-正確執行回傳資料", res)
})
.catch((error) => {
console.error("異步事件-異常訊息:", error)
})

顯示如下(透過.catch()捕獲異步事件錯誤訊息):

1
異步事件-異常訊息: { Status: 'Error' }

  • Promise的鏈式調用(多個異步事件同步執行):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function operationA(Number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ Number: Number * 2 });
}, 1000); //← 等待1秒執行
});
}
// 使用 Promise 的方式
const Nb = 20
operationA(Nb)
.then((res) => {
console.log("第一階段-異步事件-正確執行回傳資料", res)
return operationA(res.Number)
})
.then((res) => {
console.log("第二階段-異步事件-正確執行回傳資料", res)
})
.catch((error) => {
console.error("異步事件-異常訊息:", error)
})

顯示如下(確保第一個事件執行完接收res.Number,進行第二次異步事件):

1
2
第一階段-異步事件-正確執行回傳資料 { Number: 40 }
第二階段-異步事件-正確執行回傳資料 { Number: 80 }

Promise靜態方法

Promise.resolve(value) & Promise.reject(reason)

  • Promise.resolve(value):回傳Promise已完成的結果
  • Promise.reject(reason):回傳Promise被拒絕的結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.resolve("Reject Result")
.then((res) => {
console.log("Resolved:", res); // 這裡會執行
})
.catch((error) => {
console.log("Rejected:", error); // 這裡不會執行
});

Promise.reject("Reject Result")
.then((res) => {
console.log("Resolved:", res); // 這裡不會執行
})
.catch((error) => {
console.log("Rejected:", error); // 這裡會執行
});

顯示如下:

1
2
Resolved: Reject Result
Rejected: Reject Result

Promise.all(iterable)

  • 等待所有Promise狀態轉變成Fulfiled取得的結果,返回一個包含所有解析值的數組。
  • 如果其中一個Promise狀態轉變成Rejected,整個Promise.all()都會被拒絕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function operationA() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Operation A completed");
resolve({ Statue: "A" });
}, 1000); //← 等待1秒執行
});
}
function operationB() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Operation B completed");
resolve({ Statue: "B" });
}, 1500); //← 等待1.5秒執行
});
}
Promise.all([operationA(), operationB()])
.then((res) => {
console.log("res", res)
})
.catch((error) => {
console.error("Error", error);
});

顯示如下:

1
2
3
Operation A completed
Operation B completed
res [ { Statue: 'A' }, { Statue: 'B' } ]

Promise.race(iterable)

  • 只要其中一個Promise狀態轉變成Fulfiled或Rejected立即結束,返回第一個成功(resolve)或被拒絕(reject)的Promise。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function operationA() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ Statue: "A" });
}, 500); //← 等待0.5秒執行
});
}
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({ Statue: "Timeout" });
}, ms);
});
}
Promise.race([operationA(), timeout(1000)])
.then((res) => {
console.log("res", res)
})
.catch((error) => {
console.error("Error", error);
});

顯示如下:

  • 如果超過timeout的時間就算是超時
1
res { Statue: 'A' }

async/await

  • 由async函數及await運算子組成,用於處理異步操作的語法糖。
  • await會解析Promise中的Fulfiled完成狀態回傳值
  • 將async/await用法與Promise鍊式寫法相互比較案例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function operationA(Number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ Number: Number * 2 });
}, 1000); //← 等待1秒執行
});
}
// 使用 Promise 鍊式的寫法
// const Nb = 20
// operationA(Nb)
// .then((res) => {
// console.log("第一階段-異步事件-正確執行回傳資料", res)
// return operationA(res.Number)
// })
// .then((res) => {
// console.log("第二階段-異步事件-正確執行回傳資料", res)
// })
// .catch((error) => {
// console.error("異步事件-異常訊息:", error)
// })
// 使用Async/await的寫法

async function fetchData() {
try {
const Nb = 20;
const result1 = await operationA(Nb);
console.log("第一階段-異步事件-正確執行回傳資料", result1);
const result2 = await operationA(result1.Number);
console.log("第二階段-異步事件-正確執行回傳資料", result2);
} catch (error) {
console.error("異步事件-異常訊息:", error);
}
}

fetchData();

顯示如下:

1
2
第一階段-異步事件-正確執行回傳資料 { Number: 40 }
第二階段-異步事件-正確執行回傳資料 { Number: 80 }