再談 JavaScript 異步編程

jopen 9年前發布 | 17K 次閱讀 JavaScript開發 JavaScript

 

隨著前端的發展, 異步 這個詞真是越來越常見了。假設我們現在有這么一個異步任務:

向服務器發起數次請求,每次請求的結果作為下次請求的參數。

來看看我們都有哪些處理方法:

Callbacks

最先想到也是最常用的便是回調函數了,我們來進行簡單的封裝:

let makeAjaxCall = (url, cb) => {  
  // do some ajax
  // callback with result
}
makeAjaxCall('http://url1', (result) => {  
  result = JSON.parse(result)
})

嗯,看起來還不錯!但是當我們嘗試嵌套多個任務時,代碼看起來會是這樣的:

makeAjaxCall('http://url1', (result) => {  
  result = JSON.parse(result)
  makeAjaxCall(`http://url2?q=${result.query}`, (result) => {
    result = JSON.parse(result)
    makeAjaxCall(`http://url3?q=${result.query}`, (result) => {
      // ...
    })
  })
})

天哪!快讓那堆})見鬼去吧!

于是,我們想嘗試借助 JavaScript 事件模型:

Pub/Sub

在 DOM 事件的處理中,Pub/Sub 是一種很常見的機制,比如我們要為元素加上事件監聽:

elem.addEventListener(type, (evt) => {  
    // handler
})

所以我們是不是也可以構造一個類似的模型來處理異步任務呢?

首先是要構建一個分發中心,并添加on/emit方法:

let PubSub = {  
  events: {},
  on(type, handler) {
    let events = this.events
    events[type] = events[type] || []
    events[type].push(handler)
  },
  emit(type, ...datas) {
    let events = this.events
    if (!events[type]) {
      return
    }
    events[type].forEach((handler) => handler(...datas))
  }
}

然后我們便可以這樣使用:
const urls = [  
  'http://url1',
  'http://url2',
  'http://url3'
]
let makeAjaxCall = (url) => {  
  // do some ajax
  PubSub.emit('ajaxEnd', result)
}
let subscribe = (urls) => {  
  let index = 0
  PubSub.on('ajaxEnd', (result) => {
    result = JSON.parse(result)
    if (urls[++index]) {
      makeAjaxCall(`${urls[index]}?q=${result.query}`)
    }
  })
  makeAjaxCall(urls[0])
}

嗯……比起回調函數好像沒有什么革命性的改變,但是這樣做的好處是:我們可以將請求和處理函數放在不同的模塊中,減少耦合。

Promise

真正帶來革命性改變的是 Promise 規范 [1] 。借助 Promise,我們可以這樣完成異步任務:

let makeAjaxCall = (url) => {  
  return new Promise((resolve, reject) => {
    // do some ajax
    resolve(result)
  })
}
makeAjaxCall('http://url1')  
  .then(JSON.parse)
  .then((result) => makeAjaxCall(`http://url2?q=${result.query}`))
  .then(JSON.parse)
  .then((result) => makeAjaxCall(`http://url3?q=${result.query}`))

好棒!寫起來像同步處理的函數一樣!

別著急,少年。我們還有更棒的:

Generators

ES6 的另外一個大殺器便是Generators [2] 。在一個generator function中,我們可以通過yield語句來中斷函數的執行,并在函數外部通過next方法來迭代語句,更重要的是我們可以通過next方法向函數內部注入數據,動態改變函數的行為。比如:

function* gen() {  
  let a = yield 1
  let b = yield a * 2
  return b
}
let it = gen()
it.next() // output: {value: 1, done: false}  
it.next(10) // a = 10, output: {value: 20, done: false}  
it.next(100) // b = 100, output: {value: 100, done: true}  

通過generator將我們之前的makeAjaxCall函數進行封裝:
let makeAjaxCall = (url) => {  
  // do some ajax
  iterator.next(result)
}
function* requests() {  
  let result = yield makeAjaxCall('http://url1')
  result = JSON.parse(result)
  result = yield makeAjaxCall(`http://url2?q=${result.query}`)
  result = JSON.parse(result)
  result = yield makeAjaxCall(`http://url3?q=${result.query}`)
}
let iterator = requests()  
iterator.next() // get everything start  

哦!看起來邏輯很清楚的樣子,但是每次都得從外部注入iterator感覺好不舒服……

別急,我們讓Promise和Generator混合一下,看會產出什么黑魔法:

let makeAjaxCall = (url) => {  
  return new Promise((resolve, reject) => {
    // do some ajax
    resolve(result)
  })
}
let runGen = (gen) => {  
  let it = gen()
  let continuer = (value) => {
    let ret
    try {
      ret = it.next(value)
    } catch (e) {
      return Promise.reject(e)
    }
    if (ret.done) {
      return ret.value
    }
    // 將 onFulfiled, onRejected 綁定在同一個 then 上
    // 以使內部沒有進行錯誤捕獲時能拋到外部
    return Promise
      .resolve(ret.value)
      .then(continuer, (e) => it.throw(e))
  }
  return continuer()
}
function* requests() {  
  let result = yield makeAjaxCall('http://url1')
  result = JSON.parse(result)
  result = yield makeAjaxCall(`http://url2?q=${result.query}`)
  result = JSON.parse(result)
  result = yield makeAjaxCall(`http://url3?q=${result.query}`)
}
runGen(requests)  

runGen函數看起來像個自動機一樣,好厲害!

實際上,這個runGen的方法是對 ECMAScript 7async function的一個實現:

async function

ES7 中,引入了一個更自然的特性async function [3] 。利用async function我們可以這樣完成任務:

let makeAjaxCall = (url) => {  
  return new Promise((resolve, reject) => {
    // do some ajax
    resolve(result)
  })
}
;(async () => {
  let result = await makeAjaxCall('http://url1')
  result = JSON.parse(result)
  result = await makeAjaxCall(`http://url2?q=${result.query}`)
  result = JSON.parse(result)
  result = await makeAjaxCall(`http://url3?q=${result.query}`)
}())

就像我們在上文把Promise和Generator結合在一起時一樣,await關鍵字后同樣接受一個Promise。在async function中,只有在await后的語句完成后剩下的語句才會被執行,整個過程就像我們用runGen函數封裝Generator一樣。

總結

以上就是筆者總結的幾種 JavaScript 異步編程模式。在行文過程中,我們只是簡單描述了這幾種模式,并沒有提及錯誤處理的過程,您要是對此感興趣,可以參考下文列出的引用文章。

(全文完)

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!