本篇文章會介紹 javascript 的非同步程式設計發展,從最初的 Callback 技術,到 2015 年的 Promise (ES6),到 2017年的 async/await (ES8)。

如果你還沒有理解非同步程式設計,可以參考同步非同步非同步的 JavaScript 介紹

  • 一個貫穿全文的例子
  • 同步程式設計逛夜市
  • Callback 逛夜市
  • Promise 逛夜市
  • Promise.all 逛夜市
  • async/await 逛夜市
  • 比較各個方法
  • 總結
  • 參考資料

一個貫穿全文的例子

有一天你去逛夜市,逛夜市一定要買雞排和珍奶

1
2
3
4
5
6
7
class Food {
name = '雞排'
}

class Drink {
name = '珍奶'
}

而且雞排和珍奶都到手你才會開動,換句話說當你買完雞排你不會先吃掉,一定會等到珍奶也買好。

1
2
3
function eat(food, drink) { // 開動
console.log(food.name, drink.name)
}

同步程式設計逛夜市 (code)

買雞排和買珍奶都要排隊各 2 秒,使用同步等待,也就是人在現場等候

1
2
3
4
5
6
7
8
9
function wait(second) {
const ms = second * 1000
const start = new Date()
while (true) {
if (new Date() - start > ms) {
break
}
}
}

先去買雞排,再去買珍奶,開動的時候已經等 4 秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function orderFood() { // 買雞排
wait(2) // 同步等待 2 秒
return new Food()
}

function orderDrink() { // 買珍奶
wait(2) // 同步等待 2 秒
return new Drink()
}

function main() {
const food = orderFood()
const drink = orderDrink()
eat(food, drink)
}

Callback 逛夜市 (code)

夜市攤販引進協助後續服務,人不用現場等,攤販備餐完,攤販呼叫後續事項

1
2
3
4
5
6
7
8
9
10
11
12
function wait(second, callback, arg) {
const ms = second * 1000
setTimeout(callback, ms, arg)
}

function orderFood(callback) { // 買雞排
wait(2, callback, new Food()) // 非同步等待 2 秒,完成後呼叫後續函式
}

function orderDrink(callback) { // 買珍奶
wait(2, callback, new Drink()) // 非同步等待 2 秒,完成後呼叫後續函式
}

而你打算接到電話後,去看看是不是雞排珍奶都完成

1
2
3
4
5
6
7
8
let food = undefined
let drink = undefined

function callme() {
if (food !== undefined && drink !== undefined) {
eat(food, drink)
}
}

你告訴雞排攤位,做好了放在那邊,並打電話告訴你 (callme)

1
2
3
4
function setFoodAndCallme(newFood) {
food = newFood
callme()
}

你也告訴珍奶攤位一樣的事情

1
2
3
4
function setDrinkAndCallme(newDrink) {
drink = newDrink
callme()
}

Callback 出發逛夜市,由於你點完雞排立刻點珍奶,然後等他們通知,只用 2 秒多一些就拿完雞排珍奶

1
2
3
4
function main() {
orderFood(setFoodAndCallme)
orderDrink(setDrinkAndCallme)
}

Promise 逛夜市 (code)

有一天攤販決定,不想處理後續事項,那件事不該是攤販的工作,所以餐點完成後讓你自己處理後續事項

其中 resolve 是指非同步處理成功,可以使用這個函式繳交回傳值,例如: resolve(arg) 代表回傳 arg

1
2
3
4
5
6
7
8
9
10
11
12
function wait(second, arg) {
const ms = second * 1000
return new Promise((resolve, reject) => setTimeout(() => resolve(arg), ms));
}

function orderFood() { // 買雞排
return wait(2, new Food()) // 非同步等待 2 秒,完成後回傳物件
}

function orderDrink() { // 買珍奶
return wait(2, new Drink()) // 非同步等待 2 秒,完成後回傳物件
}

既然攤販不處理後續事項,那就自己處理,攤販做好餐點告訴你 (.then())

.then() 的參數是一個函式,函式裡是否有參數根據 resolve 決定,上面會帶 arg 進去作為參數,所以就接一個 arg

處理時間和 Callback 大略相同,但是最大不同點是,攤販不需要知道後續事項,交給你自己處理

1
2
3
4
function main() {
orderFood().then(arg => setFoodAndCallme(arg))
orderDrink().then(arg => setDrinkAndCallme(arg))
}

Promise.all 逛夜市 (code)

由於雞排珍奶不同攤販,他們完成的時間不一樣,所以前面才需要 setFood 和 setDrink

今天你希望當雞排、珍奶都完成時,再通知我去處理,直接移除 setFoodAndCallme, setDrinkAndCallmecallme

其中 [food, drink] 採用解構賦值

1
2
3
4
function main() {
Promise.all([orderFood(), orderDrink()])
.then(([food, drink]) => eat(food, drink))
}

async/await 逛夜市 (code)

今天客人希望當雞排、珍奶都完成時,直接送給客人,從 Promise.all 小改,就可以在 main 呼叫 eat,而不是 then

main 函式有加 async,Promise 前面有加 await

1
2
3
4
async function main() {
const [food, drink] = await Promise.all([orderFood(), orderDrink()])
eat(food, drink)
}

比較各個方法

wait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 同步 */
function wait(second) {
const ms = second * 1000
const start = new Date()
while (true) {
if (new Date() - start > ms) {
break
}
}
}

/* Callback */
function wait(second, callback, arg) {
const ms = second * 1000
setTimeout(callback, ms, arg)
}

/* Promise, Promise.all, async/await */
function wait(second, arg) {
const ms = second * 1000
return new Promise((resolve, reject) => setTimeout(() => resolve(arg), ms));
}

order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 同步 */
function orderFood() { // 買雞排
wait(2) // 同步等待 2 秒
return new Food()
}

/* Callback */
function orderFood(callback) { // 買雞排
wait(2, callback, new Food()) // 非同步等待 2 秒,完成後呼叫後續函式
}

/* Promise, Promise.all, async/await */
function orderFood() { // 買雞排
return wait(2, new Food()) // 非同步等待 2 秒,完成後回傳物件
}

set and call

同步和 Promise.all 不需要這個部分,這個方法會讓程式變得複雜,可以的話使用 Promise.all 處理會更好

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Callback, Promise */
let food = undefined

function setFoodAndCallme(newFood) {
food = newFood
callme()
}

function callme() {
if (food !== undefined && drink !== undefined) {
eat(food, drink)
}
}

main

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 main() {
const food = orderFood()
const drink = orderDrink()
eat(food, drink)
}

/* Callback */
function main() {
orderFood(setFoodAndCallme)
orderDrink(setDrinkAndCallme)
}

/* Promise */
function main() {
orderFood().then(arg => setFoodAndCallme(arg))
orderDrink().then(arg => setDrinkAndCallme(arg))
}

/* Promise.all */
function main() {
Promise.all([orderFood(), orderDrink()])
.then(([food, drink]) => eat(food, drink))
}

/* async/await */
async function main() {
const [food, drink] = await Promise.all([orderFood(), orderDrink()])
eat(food, drink)
}

總結

  • 同步程式設計就如同人在現場候餐
  • 非同步程式設計,減少等待時間也讓等候時間可以處理其他事情
  • Callback, Promise 和 async/await 是非同步程式設計的技術
  • Callback 是將後續事項交給非同步函式,請他完成的時候呼叫 (將後續事項委託攤販)
  • Promise 消除非同步函式和 Callback 的耦合 (攤販不需要處理客人後續事項)
  • Promise.all 提供整合多個非同步函式的功能 (兩者都完成再通知我)
  • async/await 將回傳值取回到呼叫者,讓非同步程式設計概念更接近同步程式設計 (兩者都完成直接傳回給你)

參考資料