再談 JavaScript 異步編程

隨着前端的發展,異步這個詞真是愈來愈常見了。假設咱們如今有這麼一個異步任務:javascript

向服務器發起數次請求,每次請求的結果做爲下次請求的參數。前端

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

Callbacks

最早想到也是最經常使用的即是回調函數了,咱們來進行簡單的封裝:es6

javascriptlet makeAjaxCall = (url, cb) => {
    // do some ajax
    // callback with result
}

makeAjaxCall('http://url1', (result) => {
    result = JSON.parse(result)
})

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

javascriptmakeAjaxCall('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 事件模型:promise

Pub/Sub

在 DOM 事件的處理中,Pub/Sub 是一種很常見的機制,好比咱們要爲元素加上事件監聽:服務器

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

因此咱們是否是也能夠構造一個相似的模型來處理異步任務呢?異步

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

javascriptlet 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))
    }
}

而後咱們即可以這樣使用:

javascriptconst 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,咱們能夠這樣完成異步任務:

javascriptlet 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 方法向函數內部注入數據,動態改變函數的行爲。好比:

javascriptfunction* 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 函數進行封裝:

javascriptlet 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 感受好不舒服……

別急,咱們讓 PromiseGenerator 混合一下,看會產出什麼黑魔法:

javascriptlet makeAjaxCall = (url) => {
    return new Promise((resolve, reject) => {
        // do some ajax
        resolve(result)
    })
}

let runGen = (gen) => {  
    let it = gen()

    let continuer = (value, err) => {
        let ret

        try {
            ret = err ? it.throw(err) : it.next(value)
        } catch (e) {
            return Promise.reject(e)
        }

        if (ret.done) {
            return ret.value
        }

        return Promise
            .resolve(ret.value)
            .then(continuer)
            .catch((e) => continuer(null, 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 7 async function 的一個實現:

async function

ES7 中,引入了一個更天然的特性 async function[3]。利用 async function 咱們能夠這樣完成任務:

javascriptlet 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}`)
})()

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

總結

以上就是筆者總結的幾種 JavaScript 異步編程模式。在行文過程當中,咱們只是簡單描述了這幾種模式,並無說起錯誤處理的過程,您要是對此感興趣,能夠參考下文列出的引用文章。

(全文完)

參考資料

  1. Promises/A+ Specification
  2. Going Async With ES6 Generators
  3. ES7 async functions
  4. Simplifying Asynchronous Coding with ES7 Async Functions
  5. 從第三方實現看 Promise

重編自個人博客,原文地址:https://idiotwu.me/going-async-with-javascript/

相關文章
相關標籤/搜索