JavaScript 非同步流程控制
發展歷史
簡要的提及一下,非同步流程控制的發展歷史大概是callback hell=>Promise=>Generator=>async/await
ES6 中Promise是通過.then().then().catch()的方式來解決callback多層巢狀的問題。但Promise依然是非同步執行的,這時候 TJ 的co,通過Generator實現了非同步程式碼的同步化。這個模式和 ES7 中的async/await類似。
function A() {
// async get dataA
function B(dataA) {
// async get dataB
function C(dataB) {
}
}
}
Promise(A).then(B).then(C).catch(err => console.log(err))
co(function *() {
var dataA = yield A()
var dataB = yield B(dataA)
var dataC = yield C(dataB)
})
async () => {
const dataA = await A()
const dataB = await B(dataA)
const dataC = await C(dataB)
}
https://www.houdianzi.com/ logo設計公司
使用
首先是語法糖支援情況,你可以使用下面命令列檢視當前 node 版本對於 ES6/ES7 的支援。目前大多瀏覽器是不支援新語法的,如果你當前環境不支援新語法,你可以使用bable、co、Promise、bluebird等開源專案來擴充套件功能。
$ node --v8-options | grep harmony
對了如果你還對這些新語法的使用方式和 API 陌生的話,建議看看《ECMAScript 6 入門》這本書,下面的內容,假定你對基本的使用已經有所瞭解,我們開始正篇。
Promise 實踐和容錯
之前當面試官的時候,如果面試物件經常使用 ES6,我會喜歡問一個問題:假設你的移動端頁面上有頭部、中部、底部三部分資料需要併發的去請求 api 拿到返回資料,你會怎麼處理?用 Promise 如何實現?如果其中一個 API 出了錯誤怎麼容錯?
1.第一個問題很簡單,依次執行三個非同步請求函式,在獲取到資料後執行渲染函式填充到頁面上
2.第二個問題,其實也沒多繞,你可以同時執行三個 Promise 函式,也可以打包成 Promise.all() 一併執行,顯然對於這種併發執行的非同步函式 Promise.all() 更符合程式設計。
const render = log = console.log
const asyncApi = (num) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num !== 'number') {
reject('param error')
}
num += 10
resolve(num)
}, 100);
})
}
asyncApi(0).then(render).catch(log) // 10
asyncApi(5).then(render).catch(log) // 15
asyncApi(10).then(render).catch(log) // 20
Promise.all([asyncApi(0), asyncApi(5), asyncApi(10)]).then(render).catch(log) // [ 10, 15, 20 ]
3.無論怎樣,我會把面試者引導到 Promise.all() 上,這時候我會丟擲問題如果其中一個 API 出了錯誤怎麼容錯?
asyncApi(0).then(render).catch(log) // 10
asyncApi(false).then(render).catch(log) // param error
asyncApi(10).then(render).catch(log) // 20
Promise.all([asyncApi(0), asyncApi(false), asyncApi(10)]).then(render).catch(log) // param error
對比發現,Promise 之間互不影響。但由於 Promise.all() 其實是將傳入的多個 Promise 打包成一個,任何一個地方出錯了都會直接丟擲異常,導致不執行then直接跳到了catch,丟失了成功的資料。
4.解決方式是使用resolve傳遞錯誤,then 環節去處理
const render = log = console.log
const asyncApi = (num) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num !== 'number') {
resolve({ err: 'param error' }) // 修改前:reject('param error')
}
num += 10
resolve({ data: num }) // 修改前:resolve(num)
}, 100);
})
}
Promise.all([asyncApi(0), asyncApi(false), asyncApi(10)]).then(render).catch(log)
// [ { data: 10 }, { err: 'param error' }, { data: 20 } ],這時候就可以區分處理了
複雜環境
我們假設一個如下的複雜場景,非同步請求之間相互依賴。僅僅用Promise來實現,會不停的呼叫then、return並且建立匿名函式。
// 流程示意圖
// data data1
// asyncApi -----> asyncApi -----> render/error
// 10 + data data2 data3
// -----> asyncApi -----> asyncApi -----> render/error
asyncApi(0).then(data => {
return Promise.all([asyncApi(data.data), asyncApi(10 + data.data)])
}).then(([data1, data2]) => {
render(data1)
return asyncApi(data2.data)
}).then(render).catch(log)
而如果加上async/await來改寫它,就可以完全按同步的寫法來獲取非同步資料,並且語義清晰。
const run = async () => {
let data = await asyncApi(0)
let [data1, data2] = await Promise.all([asyncApi(data.data), asyncApi(10 + data.data)])
render(data1)
let data3 = await asyncApi(data2.data)
render(data3)
}
run().catch(log)
或許你覺得差不了太多,那我再改一下,現在我們看到data3是需要data2作為函式引數才能獲取,假如:獲取data3需要data和data2呢?
你會發現Promise的寫法隔離了環境,如果需要data這個值,那就要想辦法傳遞下去或儲存到其他地方,而async/await的寫法就不需要考慮這個問題。
總結
在本文的前半部分簡單介紹了流程控制的發展歷史和如何使用這些新的語法糖,後半部分我們聊到了Promise和async/await如何去實現複雜的非同步流程環境,並滿足容錯和可讀性。
做一個有追求的程式設計師,在實際專案中多去思考容錯和可讀性,相信程式碼質量會有不錯的提升。