天天看點

再談異步

小親岡 愛屋吉屋 前端開發工程師

按順序完成異步操作

實際開發中,經常遇到一組異步操作,需要按照順序完成。比如,展示頁面中有上中下三個部分,每一部分通過一個接口獲得資料後就展示該部分區域内容,要求這三部分要自上而下顯示,避免下面部分先展示,然後上面部分突然“竄出”影響體驗。

思考點

  1. 接口調用應該并行送出請求,而不是按順序繼發。
  2. 接口請求可能出現異常,每個接口的異常處理不盡相同,應該分開處理。
  3. 如果接口依次傳回結果,當然可以直接展示資料。但是,如果後面部分的接口先傳回結果,應該等前面接口結果傳回并展示後才能展示。
  4. 各部分接口的請求和處理最好放在一起。

嘗試解

// 模拟API請求接口function fetch (api, ms, err = false) {  return new Promise(function (resolve, reject) {    console.log(`fetch-${api}-${ms} start`)    console.timeEnd('fetch')    setTimeout(function () {      err ? reject(`reject-${api}-${ms}`) : resolve(`resolve-${api}-${ms}`)    }, ms)  })}// 解法一function loadData () {  const promises = [fetch('API1', 3000), fetch('API2', 2000, true), fetch('API3', 5000)]  promises.reduce((chain, promise, index) => {    return chain.then(() => promise).then(data => console.log(data)).catch(err => console.error(err))  }, Promise.resolve())}// 解法二async function loadData () {  const promises = [fetch('API1', 3000), fetch('API2', 2000, true), fetch('API3', 5000)]  for (const promise of promises) {    try {      const data = await promise      console.log(data)    } catch (err) {      console.error(err)    }  }}      

這兩種解法都是可以的,但是确不能很好地将各部分接口的請求和處理放在一起。 其實,并發請求就是 

fetch

函數的同時調用,但是傳回的 

promise

确需要我們控制其按順序執行 

then

或 

catch

。是以我們可以考慮使用 

Generator

函數的暫停-恢複執行功能。

function* load1 () {  const promise = yield fetch('API1', 3000)  promise.then(function (data) {    console.log(data)  }).catch(function (err) {    console.error(err)  })}function* load2 () {  const promise = yield fetch('API2', 2000, true)  promise.then(function (data) {    console.log(data)  }).catch(function (err) {    console.error(err)  })}function* load3 () {  const promise = yield fetch('API3', 5000)  promise.then(function (data) {    console.log(data)  }).catch(function (err) {    console.error(err)  })}async function loadData () {  console.time('fetch')  const promises = [load1(), load2(), load3()].map(gen => ({ gen, promise: gen.next().value }))  for (const { gen, promise } of promises) {    try {      await promise    } catch (err) {      console.error('catch error')      console.timeEnd('fetch')    } finally {      console.info('finally', gen.next(promise))      console.timeEnd('fetch')    }  }}      

上述代碼,執行了 

loadData

函數後,在控制台上的一次結果如下:

fetch-API1-3000 startfetch: 19.390msfetch-API2-2000 startfetch: 22.986msfetch-API3-5000 startfetch: 26.002ms// 并發請求Uncaught (in promise) reject-API2-2000// 2000ms後API2請求出錯// 但是要等到API1請求結果傳回并處理後才能處理finally Object {value: undefined, done: true}fetch: 3023.055msresolve-API1-3000// 3000ms後API1請求結束,處理結果catch errorfetch: 3025.530msfinally Object {value: undefined, done: true}fetch: 3026.752msreject-API2-2000// 緊接着處理API2結果finally Object {value: undefined, done: true}fetch: 5029.639msresolve-API3-5000// 5000ms後API3請求結束,處理結果      

執行結果就是我們想要的。

最終解

可以将 

loadData

函數提取為一個公共的函數,供多次使用。完整代碼:

/** * 按順序加載異步請求資料(自動執行器) * @param {...GeneratorFunction()} args GeneratorFunction函數執行傳回值 * @return {Promise} 傳回一個Promise對象p。隻要請求出錯,就執行p的catch回調,否則執行then回調,回調參數為各個請求結果組成的數組 */async function loadDataInOrder (...args) {  const promises = [...args].map(gen => ({ gen, promise: gen.next().value }))  const result = []  let hasErr = false  for (const { gen, promise } of promises) {    try {      result.push(await promise)    } catch (err) {      result.push(err)      hasErr = true    } finally {      gen.next(promise)    }  }  if (hasErr) {    throw result  }  return result}// 模拟API請求接口function fetch (api, ms, err = false) {  return new Promise(function (resolve, reject) {    setTimeout(function () {      err ? reject(`reject-${api}-${ms}`) : resolve(`resolve-${api}-${ms}`)    }, ms)  })}// 請求接口1function* load1 () {  (yield fetch('API1', 3000)).then(function (res) {    console.log(res)  }).catch(function (err) {    console.error(err)  })}// 請求接口2function* load2 () {  (yield fetch('API2', 2000, true)).then(function (data) {    console.log(data)  }).catch(function (err) {    console.error(err)  })}// 請求接口3function* load3 () {  (yield fetch('API3', 5000)).then(function (data) {    console.log(data)  }).catch(function (err) {    console.error(err)  })}// 按順序加載異步請求loadDataInOrder(load1(), load2(), load3()).then(function ([data1, data2, data3]) {  console.log('ok', data1, data2, data3)}).catch(function ([err1, err2, err3]) {  console.error('error', err1, err2, err3)})