Concurrency
並行的非同步任務本質上是這樣一個問題:單個非同步任務僅可能返回兩種狀態:正常、異常。假設有 m(m > 1)個併發執行的非同步任務合成一個非同步任務集合,那麼這個集合(也是一個非同步任務)應該返回什麼?
- 全部正常才正常(只要有一個子任務異常,就返回異常)
- 全部正常時,返回所有子任務結果的集合:
Promise.all
- 全部正常時,返回最先完成的子任務的結果:
Promise.race
- 全部正常時,返回所有子任務結果的集合:
- 存在正常就正常
- 不管有沒有異常,等所有子任務結束,返回所有結果的集合:
Promise.settle
- 不管有沒有異常,只要出現一個子任務正常,就返回該任務的結果:
Promise.any
- 不管有沒有異常,等所有子任務結束,返回所有結果的集合:
- 存在 n 個正常才正常,否則返回異常:
Promise.some
Sequence
序列化非同步問題是說:有些非同步任務不能同時執行(互斥關係),必須等上一個執行完,才能執行下一個。來看幾個典型場景:
1、下載佇列問題
比如使用者可以勾選任意多個檔案並下載,假設我們的策略是下載完第一個再下載第二個,這種任務應該怎麼實現呢?
可以藉助某種佇列或迴圈機制,比如通過 reduce 或 for of 來實現:
寫法一:
寫法二:
寫法三:
市面上也有一些現成的庫可以處理這種問題,比如 async.series、deferred-queue、promise-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
複製程式碼