非同步任務:並行與序列的典型問題

白勝發表於2019-05-06

Concurrency

並行的非同步任務本質上是這樣一個問題:單個非同步任務僅可能返回兩種狀態:正常、異常。假設有 m(m > 1)個併發執行的非同步任務合成一個非同步任務集合,那麼這個集合(也是一個非同步任務)應該返回什麼?

  • 全部正常才正常(只要有一個子任務異常,就返回異常)
    • 全部正常時,返回所有子任務結果的集合:Promise.all
    • 全部正常時,返回最先完成的子任務的結果:Promise.race
  • 存在正常就正常
    • 不管有沒有異常,等所有子任務結束,返回所有結果的集合:Promise.settle
    • 不管有沒有異常,只要出現一個子任務正常,就返回該任務的結果:Promise.any
  • 存在 n 個正常才正常,否則返回異常:Promise.some

Sequence

序列化非同步問題是說:有些非同步任務不能同時執行(互斥關係),必須等上一個執行完,才能執行下一個。來看幾個典型場景:

1、下載佇列問題

比如使用者可以勾選任意多個檔案並下載,假設我們的策略是下載完第一個再下載第二個,這種任務應該怎麼實現呢?

可以藉助某種佇列或迴圈機制,比如通過 reduce 或 for of 來實現:

寫法一:

非同步任務:並行與序列的典型問題

寫法二:

非同步任務:並行與序列的典型問題

寫法三:

非同步任務:並行與序列的典型問題

市面上也有一些現成的庫可以處理這種問題,比如 async.seriesdeferred-queuepromise-sequence 等。

2、loading 問題

假設每個介面請求發起時都會展示 loading,請求結束隱藏 loading。介面請求可能有很多,但每時每刻介面上只能有一個 loading。比如 a 請求發出,展示 loading,之後 b 請求發出,如果 a 請求結束時,b 還沒有結束,那麼繼續展示 loading,反之則隱藏 loading,這怎麼實現呢?

可以考慮一種引用計數的策略:

var loading = {
    count: 0,
    el: document.createTextNode('loading'),
    start () {
        if (this.count === 0) {
            document.body.appendChild(this.el)
        }
        this.count += 1
    },
    
    stop () {
        this.count -= 1
        if (this.count === 0) {
            document.body.removeChild(this.el)
        }
    }
}
複製程式碼

3、競態問題

競態問題是指同一類請求,先後傳送,以哪一個的返回為準?比如使用者搜尋 A 類電影,由於介面遲遲未返回,使用者選擇搜尋 B 類電影,如果 B 的請求還沒有返回,A 卻返回了,這時怎麼辦?

每個操作都只是單個非同步任務,而不是一個序列任務,但使用者的多次操作卻構成了一個序列任務。

顯然只有最新的請求才應該被使用,我們可以用時間戳來標識每個請求。

const map = {
    'fetchMovie': 0
}
function fetchMovie () {
    const stamp = Date.now()
    map.fetchMovie = stamp
    fetch(url, params).then(res => {
        if (stamp < map.fetchMovie) return null // 該請求已過時
        return res
    })
}
複製程式碼

不過這種方案侵入性比較強,能不能實現一個類似 redux-saga 中的 takeLatest 的方法呢?takeLatest 的基本思路是:只要有最新的請求,就將之前的請求 cancel 掉,但 promise 沒有辦法 cancel(I know bluebird),這怎麼辦呢?

// 模擬一個在 t 時間後返回結果的介面請求
function request (t) {
    return new Promise(resolve => {
        setTimeout(function () {
            resolve(t)
        }, t)
    })
}

const map = {}

function takeLatest (key, fn) {
    if (!map[key]) {
        map[key] = 1
    }

    return function () {
        let resolve
        let reject
        
        // 嘿嘿
        const a = new Promise((_res, _rej) => {
            resolve = _res
            reject = _rej
        })
    
        const t = Date.now()
        map[key] = t

        fn.apply(null, arguments).then(res => {
            if (t < map[key]) return
            resolve(res)
        }).catch(error => {
            if (t < map[key]) return
            reject(error)
        })
        
        return a
    }
}

// 測試
const f1 = takeLatest('fetchMovie', request)
const f2 = takeLatest('fetchOther', request)

f1(3000).then(res => {
    console.log(res)
})

f2(3050).then(res => {
    console.log(res)
})

setTimeout(() => {
    f1(1000).then(res => {
        console.log(res)
    })
    
    f2(1050).then(res => {
        console.log(res)
    })
}, 1000)

// 返回 setTimeout 裡的兩個“最新”的請求結果:1000,1050
複製程式碼

相關文章