Javascript非同步處理 / Promise / Async Await

Posted by Benjamin Lu on 2017-08-22

Javascript非同步處理 / Promise / Async Await

Node.js線程模型

單線程且非同步?

請參閱資料

非同步程式碼之霧:Node.js 的事件迴圈與 EventEmitter

Javascript非同步程式與處理Callback Hell

使用setTimeout模擬ajax去server side取得message的資料後
每一個工作依賴於前一個先做完才能往下做

  1. 顯示取回的資料
  2. 跟之前的資料做加總
  3. 最後顯示總和
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
setTimeout(function () {
var message = 1 // 取回資料
console.log(message)
var sum = message // 加總
// 第一個請求結束,開始下一個請求
setTimeout(function () {
var message = 1 // 取回資料
console.log(message)
sum += message // 跟之前的資料做加總
// 第二個請求結束,開始下一個請求
setTimeout(function () {
var message = 1 // 取回資料
console.log(message)
sum += message // 跟之前的資料做加總
// 第三個請求結束,開始下一個請求
setTimeout(function () {
// 第四個請求結束
console.log(message) // 取回資料
sum += message // 加總
console.log(sum) // 做最後的處理
}, 1000)
}, 1000)
}, 1000)
}, 1000)

如果有幾個非同步請求彼此相依,要依照順序執行時
這種一層一層弓形的callback function就會形成沒有可讀性,難維護的程式碼
稱為callback hell

例如:你有三個後端API

  1. 取得文章列表
  2. 取得某文章的回文列表
  3. 取得某回文的作者信箱

在前端就可能要依照順序呼叫這三個API就有可能寫出這樣的程式碼

如何處理?

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var sum = 0
var first = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var second = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var third = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var fourth = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
// 一個Promise做完之後印出Promise工作取得的資料,並開始下一個Promise工作
first().then(function (message) {
// 1
console.log(message) // 取回資料
sum += message // 加總
return second() // 第一個請求結束,開始第二個請求
}).then(function (message) {
// 1
console.log(message) // 取回資料
sum += message // 加總
return third() // 第二個請求結束,開始第三個請求
}).then(function (message) {
// 1
console.log(message) // 取回資料
sum += message // 加總
return fourth() // 第三個請求結束,開始第四個請求
}).then(function (message) {
// 第四個請求結束
// 1
console.log(message) // 取回資料
sum += message // 加總
// 4
console.log(sum) // 最後處理
}).catch(console.error) // 如果中間發生錯誤,做錯誤處理

除了使用ES6原生支援的Promise物件外
在不支援Promise的環境下,還有很多可以替代的套件

  1. Q
  2. jQuery Deferred Object
  3. bluebird
  4. when
  5. RSVP
  6. mmDeferred

延伸資料: Promise的規格

  1. Promise A+
  2. CommonJS Promises/A

是沒有callback hell了,但是程式碼一堆then,也不是很容易看懂
有沒有更簡單的寫法?

先談ES6 Generator與yield

*modifier標注function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function *gen() {
console.log('start')
yield "first"
yield "second"
}
// 還不會執行方法,而是返回一個generator物件
var g = gen()
console.log(g)
// 顯示start,得到yield的值,並回傳一個物件,yield的值被存在a.value中
// {"done": false, "value": "first"}
var a = g.next()
console.log(a)
// {"done": false, "value": "second"}
var a = g.next()
console.log(a)
// 再執行一次
a = g.next()
// 顯示{"done": true,"value": undefined}
console.log(a)

跟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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
var sum = 0
var first = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var second = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var third = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var fourth = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
function *gen () {
yield first()
yield second()
yield third()
yield fourth()
}
// 改寫上方generator寫法成遞迴
let g = gen()
function fetch(result) {
if(result.done) {
// 全部請求結束,做最後處理
console.log(sum) // 4
} else {
// 請求尚未全部結束,取回請求結果,繼續執行下一個請求
result.value.then(function (message) {
console.log(message) // 1,取回結果
sum += message // 加總
// 遞迴呼叫
fetch(g.next()) // 做下一個請求
})
}
}
fetch(g.next()) // 執行第一個請求

這樣看起來其實語法上並沒有比較簡潔
但有其他利用generator做flow control的套件

ES7 Async Await

Babel以及Node.js 7.6以上版本支援

先談generator和yield是要說明async/await的原理

async/await是generator, yield和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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var sum = 0
var first = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var second = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var third = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var fourth = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var fetchAll = async function () {
let a = await first() // 等待第一個請求執行結束,取回結果給a
console.log(a) // 1
sum += a // 加總
let b = await second() // 等待第二個請求執行結束,取回結果給b
console.log(b) // 1
sum += b // 加總
let c = await third() // 等待第三個請求執行結束,取回結果給c
console.log(c) // 1
sum += c // 加總
let d = await fourth() // 等待第四個請求執行結束,取回結果給c
console.log(d) // 1
sum += d // 加總
// 全部請求結束
console.log(sum) // 4 做最後處理
}
fetchAll()

處理併發請求

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var first = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var second = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var third = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
var fourth = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(1)
}, 1000)
})
}
// 使用Promise.all([]),四個非同步請求會同時併發,等待全部都做完以後執行callback
Promise.all([first(), second(), third(), fourth()])
.then(function (result) {
// [1, 1, 1, 1]
console.log(result)
let sum = result.reduce((cur, next) => cur + next)
// 4
console.log(sum)
}).catch(console.error)
// 使用async/await
async function fetchAll () {
// 遠端抓取資料是可能失敗的! 記得用try/catch做error handling
try {
let result = await Promise.all([first(), second(), third(), fourth()])
// [1, 1, 1, 1]
console.log(result)
let sum = result.reduce((cur, next) => cur + next)
// 4
console.log(sum)
} catch (e) {
console.log(e)
}
}
fetchAll()