欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript 異步時(shí)序問題

 更新時(shí)間:2020年11月20日 08:53:45   作者:rxliuli  
這篇文章主要介紹了JavaScript 異步時(shí)序問題,幫助大家更好的理解和學(xué)習(xí)JavaScript,感興趣的朋友可以了解下

場景

死后我們必升天堂,因?yàn)榛顣r(shí)我們已在地獄。

不知你是否遇到過,向后臺(tái)發(fā)送了多次異步請求,結(jié)果最后顯示的數(shù)據(jù)卻并不正確 – 是舊的數(shù)據(jù)。

具體情況:

  1. 用戶觸發(fā)事件,發(fā)送了第 1 次請求
  2. 用戶觸發(fā)事件,發(fā)送了第 2 次請求
  3. 第 2 次請求成功,更新頁面上的數(shù)據(jù)
  4. 第 1 次請求成功,更新頁面上的數(shù)據(jù)

嗯?是不是感覺到異常了?這便是多次異步請求時(shí)會(huì)遇到的異步回調(diào)順序與調(diào)用順序不同的問題。

思考

  • 為什么會(huì)出現(xiàn)這種問題?
  • 出現(xiàn)這種問題怎么解決?

為什么會(huì)出現(xiàn)這種問題?

JavaScript 隨處可見異步,但實(shí)際上并不是那么好控制。用戶與 UI 交互,觸發(fā)事件及其對(duì)應(yīng)的處理函數(shù),函數(shù)執(zhí)行異步操作(網(wǎng)絡(luò)請求),異步操作得到結(jié)果的時(shí)間(順序)是不確定的,所以響應(yīng)到 UI 上的時(shí)間就不確定,如果觸發(fā)事件的頻率較高/異步操作的時(shí)間過長,就會(huì)造成前面的異步操作結(jié)果覆蓋后面的異步操作結(jié)果。

關(guān)鍵點(diǎn)

  • 異步操作得到結(jié)果的時(shí)間(順序)是不確定的
  • 如果觸發(fā)事件的頻率較高/異步操作的時(shí)間過長

出現(xiàn)這種問題怎么解決?

既然關(guān)鍵點(diǎn)由兩個(gè)要素組成,那么,只要破壞了任意一個(gè)即可。

  • 手動(dòng)控制異步返回結(jié)果的順序
  • 降低觸發(fā)頻率并限制異步超時(shí)時(shí)間

手動(dòng)控制返回結(jié)果的順序

根據(jù)對(duì)異步操作結(jié)果處理情況的不同也有三種不同的思路

  1. 后面異步操作得到結(jié)果后等待前面的異步操作返回結(jié)果
  2. 后面異步操作得到結(jié)果后放棄前面的異步操作返回結(jié)果
  3. 依次處理每一個(gè)異步操作,等待上一個(gè)異步操作完成之后再執(zhí)行下一個(gè)

這里先引入一個(gè)公共的 wait 函數(shù)

/**
 * 等待指定的時(shí)間/等待指定表達(dá)式成立
 * 如果未指定等待條件則立刻執(zhí)行
 * 注: 此實(shí)現(xiàn)在 nodejs 10- 會(huì)存在宏任務(wù)與微任務(wù)的問題,切記 async-await 本質(zhì)上還是 Promise 的語法糖,實(shí)際上并非真正的同步函數(shù)!??!即便在瀏覽器,也不要依賴于這種特性。
 * @param param 等待時(shí)間/等待條件
 * @returns Promise 對(duì)象
 */
function wait(param) {
 return new Promise(resolve => {
 if (typeof param === 'number') {
 setTimeout(resolve, param)
 } else if (typeof param === 'function') {
 const timer = setInterval(() => {
 if (param()) {
 clearInterval(timer)
 resolve()
 }
 }, 100)
 } else {
 resolve()
 }
 })
}

1. 后面異步操作得到結(jié)果后等待前面的異步操作返回結(jié)果

  1. 為每一次的異步調(diào)用都聲稱一個(gè)唯一 id
  2. 使用列表記錄所有的異步 id
  3. 在真正調(diào)用異步操作后,添加一個(gè)唯一 id
  4. 判斷上一個(gè)正在執(zhí)行的異步操作是否完成
  5. 如果未完成等待上一個(gè)異步操作完成,否則直接跳過
  6. 從列表中刪除掉當(dāng)前的 id
  7. 最后等待異步操作然后返回結(jié)果
/**
 * 將一個(gè)異步函數(shù)包裝為具有時(shí)序的異步函數(shù)
 * 注: 該函數(shù)會(huì)按照調(diào)用順序依次返回結(jié)果,后面的調(diào)用的結(jié)果需要等待前面的,所以如果不關(guān)心過時(shí)的結(jié)果,請使用 {@link switchMap} 函數(shù)
 * @param fn 一個(gè)普通的異步函數(shù)
 * @returns 包裝后的函數(shù)
 */
function mergeMap(fn) {
 // 當(dāng)前執(zhí)行的異步操作 id
 let id = 0
 // 所執(zhí)行的異步操作 id 列表
 const ids = new Set()
 return new Proxy(fn, {
 async apply(_, _this, args) {
 const prom = Reflect.apply(_, _this, args)
 const temp = id
 ids.add(temp)
 id++
 await wait(() => !ids.has(temp - 1))
 ids.delete(temp)
 return await prom
 },
 })
}

測試一下

;(async () => {
 // 模擬一個(gè)異步請求,接受參數(shù)并返回它,然后等待指定的時(shí)間
 async function get(ms) {
 await wait(ms)
 return ms
 }
 const fn = mergeMap(get)
 let last = 0
 let sum = 0
 await Promise.all([
 fn(30).then(res => {
 last = res
 sum += res
 }),
 fn(20).then(res => {
 last = res
 sum += res
 }),
 fn(10).then(res => {
 last = res
 sum += res
 }),
 ])
 console.log(last)
 // 實(shí)際上確實(shí)執(zhí)行了 3 次,結(jié)果也確實(shí)為 3 次調(diào)用參數(shù)之和
 console.log(sum)
})()

2. 后面異步操作得到結(jié)果后放棄前面的異步操作返回結(jié)果

  • 為每一次的異步調(diào)用都聲稱一個(gè)唯一 id
  • 記錄最新得到異步操作結(jié)果的 id
  • 記錄最新得到的異步操作結(jié)果
  • 執(zhí)行并等待返回結(jié)果
  • 判斷本次異步調(diào)用后面是否已經(jīng)有調(diào)用出現(xiàn)結(jié)果了

                   是的話就直接返回后面的異步調(diào)用結(jié)果
                   否則將本地異步調(diào)用 id 及其結(jié)果最為[最后的]
                   返回這次的異步調(diào)用結(jié)果

/**
 * 將一個(gè)異步函數(shù)包裝為具有時(shí)序的異步函數(shù)
 * 注: 該函數(shù)會(huì)丟棄過期的異步操作結(jié)果,這樣的話性能會(huì)稍稍提高(主要是響應(yīng)比較快的結(jié)果會(huì)立刻生效而不必等待前面的響應(yīng)結(jié)果)
 * @param fn 一個(gè)普通的異步函數(shù)
 * @returns 包裝后的函數(shù)
 */
function switchMap(fn) {
 // 當(dāng)前執(zhí)行的異步操作 id
 let id = 0
 // 最后一次異步操作的 id,小于這個(gè)的操作結(jié)果會(huì)被丟棄
 let last = 0
 // 緩存最后一次異步操作的結(jié)果
 let cache
 return new Proxy(fn, {
 async apply(_, _this, args) {
 const temp = id
 id++
 const res = await Reflect.apply(_, _this, args)
 if (temp < last) {
 return cache
 }
 cache = res
 last = temp
 return res
 },
 })
}

測試一下

;(async () => {
 // 模擬一個(gè)異步請求,接受參數(shù)并返回它,然后等待指定的時(shí)間
 async function get(ms) {
 await wait(ms)
 return ms
 }
 const fn = switchMap(get)
 let last = 0
 let sum = 0
 await Promise.all([
 fn(30).then(res => {
 last = res
 sum += res
 }),
 fn(20).then(res => {
 last = res
 sum += res
 }),
 fn(10).then(res => {
 last = res
 sum += res
 }),
 ])
 console.log(last)
 // 實(shí)際上確實(shí)執(zhí)行了 3 次,然而結(jié)果并不是 3 次調(diào)用參數(shù)之和,因?yàn)榍皟纱蔚慕Y(jié)果均被拋棄,實(shí)際上返回了最后一次發(fā)送請求的結(jié)果
 console.log(sum)
})()

3. 依次處理每一個(gè)異步操作,等待上一個(gè)異步操作完成之后再執(zhí)行下一個(gè)

  1. 為每一次的異步調(diào)用都聲稱一個(gè)唯一 id
  2. 使用列表記錄所有的異步 id
  3. 向列表中添加一個(gè)唯一 id
  4. 判斷上一個(gè)正在執(zhí)行的異步操作是否完成
  5. 如果未完成等待上一個(gè)異步操作完成,否則直接跳過
  6. 真正調(diào)用異步操作
  7. 從列表中刪除掉當(dāng)前的 id
  8. 最后等待異步操作然后返回結(jié)果
/**
 * 將一個(gè)異步函數(shù)包裝為具有時(shí)序的異步函數(shù)
 * 注: 該函數(shù)會(huì)按照調(diào)用順序依次返回結(jié)果,后面的執(zhí)行的調(diào)用(不是調(diào)用結(jié)果)需要等待前面的,此函數(shù)適用于異步函數(shù)的內(nèi)里執(zhí)行也必須保證順序時(shí)使用,否則請使用 {@link mergeMap} 函數(shù)
 * 注: 該函數(shù)其實(shí)相當(dāng)于調(diào)用 {@code asyncLimiting(fn, {limit: 1})} 函數(shù)
 * 例如即時(shí)保存文檔到服務(wù)器,當(dāng)然要等待上一次的請求結(jié)束才能請求下一次,不然數(shù)據(jù)庫保存的數(shù)據(jù)就存在謬誤了
 * @param fn 一個(gè)普通的異步函數(shù)
 * @returns 包裝后的函數(shù)
 */
function concatMap(fn) {
 // 當(dāng)前執(zhí)行的異步操作 id
 let id = 0
 // 所執(zhí)行的異步操作 id 列表
 const ids = new Set()
 return new Proxy(fn, {
 async apply(_, _this, args) {
 const temp = id
 ids.add(temp)
 id++
 await wait(() => !ids.has(temp - 1))
 const prom = Reflect.apply(_, _this, args)
 ids.delete(temp)
 return await prom
 },
 })
}

測試一下

;(async () => {
 // 模擬一個(gè)異步請求,接受參數(shù)并返回它,然后等待指定的時(shí)間
 async function get(ms) {
 await wait(ms)
 return ms
 }
 const fn = concatMap(get)
 let last = 0
 let sum = 0
 await Promise.all([
 fn(30).then(res => {
 last = res
 sum += res
 }),
 fn(20).then(res => {
 last = res
 sum += res
 }),
 fn(10).then(res => {
 last = res
 sum += res
 }),
 ])
 console.log(last)
 // 實(shí)際上確實(shí)執(zhí)行了 3 次,然而結(jié)果并不是 3 次調(diào)用參數(shù)之和,因?yàn)榍皟纱蔚慕Y(jié)果均被拋棄,實(shí)際上返回了最后一次發(fā)送請求的結(jié)果
 console.log(sum)
})()

小結(jié)

雖然三個(gè)函數(shù)看似效果都差不多,但還是有所不同的。

  1. 是否允許異步操作并發(fā)?否: concatMap, 是: 到下一步
  2. 是否需要處理舊的的結(jié)果?否: switchMap, 是: mergeMap

降低觸發(fā)頻率并限制異步超時(shí)時(shí)間

思考一下第二種解決方式,本質(zhì)上其實(shí)是 限流 + 自動(dòng)超時(shí),首先實(shí)現(xiàn)這兩個(gè)函數(shù)。

  • 限流: 限制函數(shù)調(diào)用的頻率,如果調(diào)用的頻率過快則不會(huì)真正執(zhí)行調(diào)用而是返回舊值
  • 自動(dòng)超時(shí): 如果到了超時(shí)時(shí)間,即便函數(shù)還未得到結(jié)果,也會(huì)自動(dòng)超時(shí)并拋出錯(cuò)誤

下面來分別實(shí)現(xiàn)它們

限流實(shí)現(xiàn)

具體實(shí)現(xiàn)思路可見: JavaScript 防抖和節(jié)流

/**
 * 函數(shù)節(jié)流
 * 節(jié)流 (throttle) 讓一個(gè)函數(shù)不要執(zhí)行的太頻繁,減少執(zhí)行過快的調(diào)用,叫節(jié)流
 * 類似于上面而又不同于上面的函數(shù)去抖, 包裝后函數(shù)在上一次操作執(zhí)行過去了最小間隔時(shí)間后會(huì)直接執(zhí)行, 否則會(huì)忽略該次操作
 * 與上面函數(shù)去抖的明顯區(qū)別在連續(xù)操作時(shí)會(huì)按照最小間隔時(shí)間循環(huán)執(zhí)行操作, 而非僅執(zhí)行最后一次操作
 * 注: 該函數(shù)第一次調(diào)用一定會(huì)執(zhí)行,不需要擔(dān)心第一次拿不到緩存值,后面的連續(xù)調(diào)用都會(huì)拿到上一次的緩存值
 * 注: 返回函數(shù)結(jié)果的高階函數(shù)需要使用 {@link Proxy} 實(shí)現(xiàn),以避免原函數(shù)原型鏈上的信息丟失
 *
 * @param {Number} delay 最小間隔時(shí)間,單位為 ms
 * @param {Function} action 真正需要執(zhí)行的操作
 * @return {Function} 包裝后有節(jié)流功能的函數(shù)。該函數(shù)是異步的,與需要包裝的函數(shù) {@link action} 是否異步?jīng)]有太大關(guān)聯(lián)
 */
const throttle = (delay, action) => {
 let last = 0
 let result
 return new Proxy(action, {
 apply(target, thisArg, args) {
 return new Promise(resolve => {
 const curr = Date.now()
 if (curr - last > delay) {
 result = Reflect.apply(target, thisArg, args)
 last = curr
 resolve(result)
 return
 }
 resolve(result)
 })
 },
 })
}

自動(dòng)超時(shí)

注: asyncTimeout 函數(shù)實(shí)際上只是為了避免一種情況,異步請求時(shí)間超過節(jié)流函數(shù)最小間隔時(shí)間導(dǎo)致結(jié)果返回順序錯(cuò)亂。

/**
 * 為異步函數(shù)添加自動(dòng)超時(shí)功能
 * @param timeout 超時(shí)時(shí)間
 * @param action 異步函數(shù)
 * @returns 包裝后的異步函數(shù)
 */
function asyncTimeout(timeout, action) {
 return new Proxy(action, {
 apply(_, _this, args) {
 return Promise.race([
 Reflect.apply(_, _this, args),
 wait(timeout).then(Promise.reject),
 ])
 },
 })
}

結(jié)合使用

測試一下

;(async () => {
 // 模擬一個(gè)異步請求,接受參數(shù)并返回它,然后等待指定的時(shí)間
 async function get(ms) {
 await wait(ms)
 return ms
 }
 const time = 100
 const fn = asyncTimeout(time, throttle(time, get))
 let last = 0
 let sum = 0
 await Promise.all([
 fn(30).then(res => {
 last = res
 sum += res
 }),
 fn(20).then(res => {
 last = res
 sum += res
 }),
 fn(10).then(res => {
 last = res
 sum += res
 }),
 ])
 // last 結(jié)果為 10,和 switchMap 的不同點(diǎn)在于會(huì)保留最小間隔期間的第一次,而拋棄掉后面的異步結(jié)果,和 switchMap 正好相反!
 console.log(last)
 // 實(shí)際上確實(shí)執(zhí)行了 3 次,結(jié)果也確實(shí)為第一次次調(diào)用參數(shù)的 3 倍
 console.log(sum)
})()

起初吾輩因?yàn)楹闷鎸?shí)現(xiàn)了這種方式,但原以為會(huì)和 concatMap 類似的函數(shù)卻變成了現(xiàn)在這樣 – 更像倒置的 switchMap 了。不過由此看來這種方式的可行性并不大,畢竟,沒人需要舊的數(shù)據(jù)。

總結(jié)

其實(shí)第一種實(shí)現(xiàn)方式屬于 rxjs 早就已經(jīng)走過的道路,目前被 Angular 大量采用(類比于 React 中的 Redux)。但 rxjs 實(shí)在太強(qiáng)大也太復(fù)雜了,對(duì)于吾輩而言,僅僅需要一只香蕉,而不需要拿著香蕉的大猩猩,以及其所處的整個(gè)森林(此處原本是被人吐槽面向?qū)ο缶幊痰碾[含環(huán)境,這里吾輩稍微藉此吐槽一下動(dòng)不動(dòng)就上庫的開發(fā)者)。

可以看到吾輩在這里大量使用了 Proxy,那么,原因是什么呢?這個(gè)疑問就留到下次再說吧!

以上就是JavaScript 異步時(shí)序問題的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 異步時(shí)序的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論