成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript 異步時序問題

tuantuan / 1182人閱讀

摘要:異步時序問題吾輩的博客原文場景死后我們必升天堂,因?yàn)榛顣r我們已在地獄。關(guān)鍵點(diǎn)異步操作得到結(jié)果的時間順序是不確定的如果觸發(fā)事件的頻率較高異步操作的時間過長出現(xiàn)這種問題怎么解決既然關(guān)鍵點(diǎn)由兩個要素組成,那么,只要破壞了任意一個即可。

JavaScript 異步時序問題
吾輩的博客原文:https://blog.rxliuli.com/p/de...
場景
死后我們必升天堂,因?yàn)榛顣r我們已在地獄。

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

具體情況:

用戶觸發(fā)事件,發(fā)送了第 1 次請求

用戶觸發(fā)事件,發(fā)送了第 2 次請求

第 2 次請求成功,更新頁面上的數(shù)據(jù)

第 1 次請求成功,更新頁面上的數(shù)據(jù)

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

思考

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

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

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

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

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

異步操作得到結(jié)果的時間(順序)是不確定的

如果觸發(fā)事件的頻率較高/異步操作的時間過長

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

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

手動控制異步返回結(jié)果的順序

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

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

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

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

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

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

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

/**
 * 等待指定的時間/等待指定表達(dá)式成立
 * 如果未指定等待條件則立刻執(zhí)行
 * 注: 此實(shí)現(xiàn)在 nodejs 10- 會存在宏任務(wù)與微任務(wù)的問題,切記 async-await 本質(zhì)上還是 Promise 的語法糖,實(shí)際上并非真正的同步函數(shù)?。。〖幢阍跒g覽器,也不要依賴于這種特性。
 * @param param 等待時間/等待條件
 * @returns Promise 對象
 */
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é)果
/**
 * 將一個異步函數(shù)包裝為具有時序的異步函數(shù)
 * 注: 該函數(shù)會按照調(diào)用順序依次返回結(jié)果,后面的調(diào)用的結(jié)果需要等待前面的,所以如果不關(guān)心過時的結(jié)果,請使用 {@link switchMap} 函數(shù)
 * @param fn 一個普通的異步函數(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 () => {
  // 模擬一個異步請求,接受參數(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é)果
/**
 * 將一個異步函數(shù)包裝為具有時序的異步函數(shù)
 * 注: 該函數(shù)會丟棄過期的異步操作結(jié)果,這樣的話性能會稍稍提高(主要是響應(yīng)比較快的結(jié)果會立刻生效而不必等待前面的響應(yīng)結(jié)果)
 * @param fn 一個普通的異步函數(shù)
 * @returns 包裝后的函數(shù)
 */
function switchMap(fn) {
  // 當(dāng)前執(zhí)行的異步操作 id
  let id = 0
  // 最后一次異步操作的 id,小于這個的操作結(jié)果會被丟棄
  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 () => {
  // 模擬一個異步請求,接受參數(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. 依次處理每一個異步操作,等待上一個異步操作完成之后再執(zhí)行下一個
/**
 * 將一個異步函數(shù)包裝為具有時序的異步函數(shù)
 * 注: 該函數(shù)會按照調(diào)用順序依次返回結(jié)果,后面的執(zhí)行的調(diào)用(不是調(diào)用結(jié)果)需要等待前面的,此函數(shù)適用于異步函數(shù)的內(nèi)里執(zhí)行也必須保證順序時使用,否則請使用 {@link mergeMap} 函數(shù)
 * 注: 該函數(shù)其實(shí)相當(dāng)于調(diào)用 {@code asyncLimiting(fn, {limit: 1})} 函數(shù)
 * 例如即時保存文檔到服務(wù)器,當(dāng)然要等待上一次的請求結(jié)束才能請求下一次,不然數(shù)據(jù)庫保存的數(shù)據(jù)就存在謬誤了
 * @param fn 一個普通的異步函數(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 () => {
  // 模擬一個異步請求,接受參數(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é)

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

是否允許異步操作并發(fā)?否: concatMap, 是: 到下一步

是否需要處理舊的的結(jié)果?否: switchMap, 是: mergeMap

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

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

限流: 限制函數(shù)調(diào)用的頻率,如果調(diào)用的頻率過快則不會真正執(zhí)行調(diào)用而是返回舊值

自動超時: 如果到了超時時間,即便函數(shù)還未得到結(jié)果,也會自動超時并拋出錯誤

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

限流實(shí)現(xiàn)
具體實(shí)現(xiàn)思路可見: JavaScript 防抖和節(jié)流
/**
 * 函數(shù)節(jié)流
 * 節(jié)流 (throttle) 讓一個函數(shù)不要執(zhí)行的太頻繁,減少執(zhí)行過快的調(diào)用,叫節(jié)流
 * 類似于上面而又不同于上面的函數(shù)去抖, 包裝后函數(shù)在上一次操作執(zhí)行過去了最小間隔時間后會直接執(zhí)行, 否則會忽略該次操作
 * 與上面函數(shù)去抖的明顯區(qū)別在連續(xù)操作時會按照最小間隔時間循環(huán)執(zhí)行操作, 而非僅執(zhí)行最后一次操作
 * 注: 該函數(shù)第一次調(diào)用一定會執(zhí)行,不需要擔(dān)心第一次拿不到緩存值,后面的連續(xù)調(diào)用都會拿到上一次的緩存值
 * 注: 返回函數(shù)結(jié)果的高階函數(shù)需要使用 {@link Proxy} 實(shí)現(xiàn),以避免原函數(shù)原型鏈上的信息丟失
 *
 * @param {Number} delay 最小間隔時間,單位為 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)
      })
    },
  })
}
自動超時
注: asyncTimeout 函數(shù)實(shí)際上只是為了避免一種情況,異步請求時間超過節(jié)流函數(shù)最小間隔時間導(dǎo)致結(jié)果返回順序錯亂。
/**
 * 為異步函數(shù)添加自動超時功能
 * @param timeout 超時時間
 * @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 () => {
  let last = 0
  let sum = 0
  // 模擬一個異步請求,接受參數(shù)并返回它,然后等待指定的時間
  async function get(ms) {
    await wait(ms)
    return ms
  }
  const time = 100
  const fn = asyncTimeout(time, throttle(time, get))
  await Promise.all([
    fn(30).then(res => {
      console.log(res, last, sum)
      last = res
      sum += res
    }),
    fn(20).then(res => {
      console.log(res, last, sum)
      last = res
      sum += res
    }),
    fn(10).then(res => {
      console.log(res, last, sum)
      last = res
      sum += res
    }),
  ])
  // last 結(jié)果為 10,和 switchMap 的不同點(diǎn)在于會保留最小間隔期間的第一次,而拋棄掉后面的異步結(jié)果,和 switchMap 正好相反!
  console.log(last)
  // 實(shí)際上確實(shí)執(zhí)行了 3 次,結(jié)果也確實(shí)為第一次次調(diào)用參數(shù)的 3 倍
  console.log(sum)
})()

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

總結(jié)

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

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

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/104673.html

相關(guān)文章

  • javascript知識點(diǎn)

    摘要:模塊化是隨著前端技術(shù)的發(fā)展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調(diào)也不等同于異步。將會討論安全的類型檢測惰性載入函數(shù)凍結(jié)對象定時器等話題。 Vue.js 前后端同構(gòu)方案之準(zhǔn)備篇——代碼優(yōu)化 目前 Vue.js 的火爆不亞于當(dāng)初的 React,本人對寫代碼有潔癖,代碼也是藝術(shù)。此篇是準(zhǔn)備篇,工欲善其事,必先利其器。我們先在代...

    Karrdy 評論0 收藏0
  • 好文 - 收藏集 - 掘金

    摘要:好吧,本文的主題可能還深入剖析的深復(fù)制前端掘金一年前我曾寫過一篇中的一種深復(fù)制實(shí)現(xiàn),當(dāng)時寫這篇文章的時候還比較稚嫩,有很多地方?jīng)]有考慮仔細(xì)。 翻譯 | 深入理解 CSS 時序函數(shù) - 前端 - 掘金作者:Nicolas(滬江前端開發(fā)工程師) 本文原創(chuàng),轉(zhuǎn)載請注明作者及出處。 各位,趕緊綁住自己并緊緊抓牢了,因?yàn)楫?dāng)你掌握了特別有趣但又復(fù)雜的CSS時序函數(shù)之后,你將會真正體驗(yàn)到豎起頭發(fā)般的...

    fobnn 評論0 收藏0
  • javascript 回調(diào)函數(shù) 整理

    摘要:回調(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外的一方調(diào)用的,用于對該事件或條件進(jìn)行響應(yīng)。若是使用回調(diào)函數(shù)進(jìn)行處理,代碼就可以繼續(xù)進(jìn)行其他任務(wù),而無需空等。參考理解回調(diào)函數(shù)理解與使用中的回調(diào)函數(shù)這篇相當(dāng)不錯回調(diào)函數(shù) 為什么寫回調(diào)函數(shù) 對于javascript中回調(diào)函數(shù) 一直處于理解,但是應(yīng)用不好的階段,總是在別人家的代碼中看到很巧妙的回調(diào),那時候會有wow c...

    xiaowugui666 評論0 收藏0
  • JavaScript 工作原理之四-事件循環(huán)及異步編程的出現(xiàn)和 5 種更好的 async/await

    摘要:函數(shù)會在之后的某個時刻觸發(fā)事件定時器。事件循環(huán)中的這樣一次遍歷被稱為一個。執(zhí)行完畢并出棧。當(dāng)定時器過期,宿主環(huán)境會把回調(diào)函數(shù)添加至事件循環(huán)隊(duì)列中,然后,在未來的某個取出并執(zhí)行該事件。 原文請查閱這里,略有改動。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第四章。 現(xiàn)在,我們將會通過回顧單線程環(huán)境下編程的弊端及如何克服這些困難以創(chuàng)建令人驚嘆...

    maochunguang 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<