天天看點

【JS】993- JavaScript 異步流程控制

【JS】993- JavaScript 異步流程控制

來源 | https://github.com/cheogo/learn-javascript

本文主要講解 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)
}           

複制

使用

首先是文法糖支援情況,你可以使用下面指令行檢視目前 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 如何去實作複雜的異步流程環境,并滿足容錯和可讀性。

做一個有追求的程式員,在實際項目中多去思考容錯和可讀性,相信代碼品質會有不錯的提升。