电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

JavaScript 異步時序問題

瀏覽:32日期:2023-10-07 16:15:39

場景

死后我們必升天堂,因為活時我們已在地獄。

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

具體情況:

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

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

思考

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

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

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

關鍵點

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

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

既然關鍵點由兩個要素組成,那么,只要破壞了任意一個即可。

手動控制異步返回結果的順序 降低觸發(fā)頻率并限制異步超時時間

手動控制返回結果的順序

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

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

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

/** * 等待指定的時間/等待指定表達式成立 * 如果未指定等待條件則立刻執(zhí)行 * 注: 此實現(xiàn)在 nodejs 10- 會存在宏任務與微任務的問題,切記 async-await 本質上還是 Promise 的語法糖,實際上并非真正的同步函數(shù)!!!即便在瀏覽器,也不要依賴于這種特性。 * @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. 后面異步操作得到結果后等待前面的異步操作返回結果

為每一次的異步調用都聲稱一個唯一 id 使用列表記錄所有的異步 id 在真正調用異步操作后,添加一個唯一 id 判斷上一個正在執(zhí)行的異步操作是否完成 如果未完成等待上一個異步操作完成,否則直接跳過 從列表中刪除掉當前的 id 最后等待異步操作然后返回結果

/** * 將一個異步函數(shù)包裝為具有時序的異步函數(shù) * 注: 該函數(shù)會按照調用順序依次返回結果,后面的調用的結果需要等待前面的,所以如果不關心過時的結果,請使用 {@link switchMap} 函數(shù) * @param fn 一個普通的異步函數(shù) * @returns 包裝后的函數(shù) */function mergeMap(fn) { // 當前執(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) // 實際上確實執(zhí)行了 3 次,結果也確實為 3 次調用參數(shù)之和 console.log(sum)})()

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

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

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

/** * 將一個異步函數(shù)包裝為具有時序的異步函數(shù) * 注: 該函數(shù)會丟棄過期的異步操作結果,這樣的話性能會稍稍提高(主要是響應比較快的結果會立刻生效而不必等待前面的響應結果) * @param fn 一個普通的異步函數(shù) * @returns 包裝后的函數(shù) */function switchMap(fn) { // 當前執(zhí)行的異步操作 id let id = 0 // 最后一次異步操作的 id,小于這個的操作結果會被丟棄 let last = 0 // 緩存最后一次異步操作的結果 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) // 實際上確實執(zhí)行了 3 次,然而結果并不是 3 次調用參數(shù)之和,因為前兩次的結果均被拋棄,實際上返回了最后一次發(fā)送請求的結果 console.log(sum)})()

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

為每一次的異步調用都聲稱一個唯一 id 使用列表記錄所有的異步 id 向列表中添加一個唯一 id 判斷上一個正在執(zhí)行的異步操作是否完成 如果未完成等待上一個異步操作完成,否則直接跳過 真正調用異步操作 從列表中刪除掉當前的 id 最后等待異步操作然后返回結果

/** * 將一個異步函數(shù)包裝為具有時序的異步函數(shù) * 注: 該函數(shù)會按照調用順序依次返回結果,后面的執(zhí)行的調用(不是調用結果)需要等待前面的,此函數(shù)適用于異步函數(shù)的內里執(zhí)行也必須保證順序時使用,否則請使用 {@link mergeMap} 函數(shù) * 注: 該函數(shù)其實相當于調用 {@code asyncLimiting(fn, {limit: 1})} 函數(shù) * 例如即時保存文檔到服務器,當然要等待上一次的請求結束才能請求下一次,不然數(shù)據(jù)庫保存的數(shù)據(jù)就存在謬誤了 * @param fn 一個普通的異步函數(shù) * @returns 包裝后的函數(shù) */function concatMap(fn) { // 當前執(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) // 實際上確實執(zhí)行了 3 次,然而結果并不是 3 次調用參數(shù)之和,因為前兩次的結果均被拋棄,實際上返回了最后一次發(fā)送請求的結果 console.log(sum)})()

小結

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

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

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

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

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

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

限流實現(xiàn)

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

/** * 函數(shù)節(jié)流 * 節(jié)流 (throttle) 讓一個函數(shù)不要執(zhí)行的太頻繁,減少執(zhí)行過快的調用,叫節(jié)流 * 類似于上面而又不同于上面的函數(shù)去抖, 包裝后函數(shù)在上一次操作執(zhí)行過去了最小間隔時間后會直接執(zhí)行, 否則會忽略該次操作 * 與上面函數(shù)去抖的明顯區(qū)別在連續(xù)操作時會按照最小間隔時間循環(huán)執(zhí)行操作, 而非僅執(zhí)行最后一次操作 * 注: 該函數(shù)第一次調用一定會執(zhí)行,不需要擔心第一次拿不到緩存值,后面的連續(xù)調用都會拿到上一次的緩存值 * 注: 返回函數(shù)結果的高階函數(shù)需要使用 {@link Proxy} 實現(xiàn),以避免原函數(shù)原型鏈上的信息丟失 * * @param {Number} delay 最小間隔時間,單位為 ms * @param {Function} action 真正需要執(zhí)行的操作 * @return {Function} 包裝后有節(jié)流功能的函數(shù)。該函數(shù)是異步的,與需要包裝的函數(shù) {@link action} 是否異步沒有太大關聯(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ù)實際上只是為了避免一種情況,異步請求時間超過節(jié)流函數(shù)最小間隔時間導致結果返回順序錯亂。

/** * 為異步函數(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), ]) }, })}

結合使用

測試一下

;(async () => { // 模擬一個異步請求,接受參數(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 結果為 10,和 switchMap 的不同點在于會保留最小間隔期間的第一次,而拋棄掉后面的異步結果,和 switchMap 正好相反! console.log(last) // 實際上確實執(zhí)行了 3 次,結果也確實為第一次次調用參數(shù)的 3 倍 console.log(sum)})()

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

總結

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

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

以上就是JavaScript 異步時序問題的詳細內容,更多關于JavaScript 異步時序的資料請關注好吧啦網(wǎng)其它相關文章!

標簽: JavaScript
相關文章:
主站蜘蛛池模板: 海峰资讯 - 专注装饰公司营销型网站建设和网络营销培训 | 高压油管,液压接头,液压附件-烟台市正诚液压附件 | 聚合甘油__盐城市飞龙油脂有限公司 | 不锈钢监控杆_监控立杆厂家-廊坊耀星光电科技有限公司 | 闪电优家-卫生间防水补漏_酒店漏水渗水维修_防水堵漏公司 | 珠海网站建设_响应网站建设_珠海建站公司_珠海网站设计与制作_珠海网讯互联 | 国产液相色谱仪-超高效液相色谱仪厂家-上海伍丰科学仪器有限公司 | 微型气象仪_气象传感器_防爆气象传感器-天合传感器大全 | 横河变送器-横河压力变送器-EJA变送器-EJA压力变送器-「泉蕴仪表」 | 意大利Frascold/富士豪压缩机_富士豪半封闭压缩机_富士豪活塞压缩机_富士豪螺杆压缩机 | 玻璃钢格栅盖板|玻璃钢盖板|玻璃钢格栅板|树篦子-长沙川皖玻璃钢制品有限公司 | 东莞喷砂机-喷砂机-喷砂机配件-喷砂器材-喷砂加工-东莞市协帆喷砂机械设备有限公司 | 定硫仪,量热仪,工业分析仪,马弗炉,煤炭化验设备厂家,煤质化验仪器,焦炭化验设备鹤壁大德煤质工业分析仪,氟氯测定仪 | 微型气象仪_气象传感器_防爆气象传感器-天合传感器大全 | 哈希PC1R1A,哈希CA9300,哈希SC4500-上海鑫嵩实业有限公司 | 世界箱包品牌十大排名,女包小众轻奢品牌推荐200元左右,男包十大奢侈品牌排行榜双肩,学生拉杆箱什么品牌好质量好 - Gouwu3.com | DNA亲子鉴定_DNA基因检测中心官方预约平台-严选好基因网 | 电机修理_二手电机专家-河北豫通机电设备有限公司(原石家庄冀华高压电机维修中心) | 船用烟火信号弹-CCS防汛救生圈-船用救生抛绳器(海威救生设备) | 变位机,焊接变位机,焊接变位器,小型变位机,小型焊接变位机-济南上弘机电设备有限公司 | AGV无人叉车_激光叉车AGV_仓储AGV小车_AGV无人搬运车-南昌IKV机器人有限公司[官网] | 网站建设-高端品牌网站设计制作一站式定制_杭州APP/微信小程序开发运营-鼎易科技 | 涂层测厚仪_光泽度仪_uv能量计_紫外辐照计_太阳膜测试仪_透光率仪-林上科技 | 纸布|钩编布|钩针布|纸草布-莱州佳源工艺纸布厂 | 网站制作优化_网站SEO推广解决方案-无锡首宸信息科技公司 | 化工ERP软件_化工新材料ERP系统_化工新材料MES软件_MES系统-广东顺景软件科技有限公司 | 精密交叉滚子轴承厂家,转盘轴承,YRT转台轴承-洛阳千协轴承 | 磨煤机配件-高铬辊套-高铬衬板-立磨辊套-盐山县宏润电力设备有限公司 | 恒温槽_恒温水槽_恒温水浴槽-上海方瑞仪器有限公司 | 胜为光纤光缆_光纤跳线_单模尾纤_光纤收发器_ODF光纤配线架厂家直销_北京睿创胜为科技有限公司 - 北京睿创胜为科技有限公司 | 液压油缸生产厂家-山东液压站-济南捷兴液压机电设备有限公司 | 淘气堡_室内儿童乐园_户外无动力儿童游乐设备-高乐迪(北京) | 发电机组|柴油发电机组-批发,上柴,玉柴,潍柴,康明斯柴油发电机厂家直销 | 砍排机-锯骨机-冻肉切丁机-熟肉切片机-预制菜生产线一站式服务厂商 - 广州市祥九瑞盈机械设备有限公司 | 江苏密集柜_电动_手动_移动_盛隆柜业江苏档案密集柜厂家 | _网名词典_网名大全_qq网名_情侣网名_个性网名 | 储气罐,真空罐,缓冲罐,隔膜气压罐厂家批发价格,空压机储气罐规格型号-上海申容压力容器集团有限公司 | 北京四合院出租,北京四合院出售,北京平房买卖 - 顺益兴四合院 | 尾轮组_头轮组_矿用刮板_厢式刮板机_铸石刮板机厂家-双驰机械 | 400电话_400电话申请_888元包年_400电话办理服务中心_400VIP网 | 热熔胶网膜|pes热熔网膜价格|eva热熔胶膜|热熔胶膜|tpu热熔胶膜厂家-苏州惠洋胶粘制品有限公司 |