深入了解javascript异步问题及解决方法
Admin 2021-10-14 群英技术资讯 454 次浏览
这篇文章主要给大家分享javascript异步的相关内容,对大家学习和理解javascript异步有一定的帮助,下文有详细的示例和解释,感兴趣的朋友可以参考,接下来跟随小编一起来学习一下吧。
场景
不知你是否遇到过,向后台发送了多次异步请求,结果最后显示的数据却并不正确- 是旧的数据。
具体情况:
嗯?是不是感觉到异常了?这便是多次异步请求时会遇到的异步回调顺序与调用顺序不同的问题。
思考
为什么会出现这种问题?
JavaScript 随处可见异步,但实际上并不是那么好控制。用户与 UI 交互,触发事件及其对应的处理函数,函数执行异步操作(网络请求),异步操作得到结果的时间(顺序)是不确定的,所以响应到 UI 上的时间就不确定,如果触发事件的频率较高/异步操作的时间过长,就会造成前面的异步操作结果覆盖后面的异步操作结果。
关键点
出现这种问题怎么解决?
既然关键点由两个要素组成,那么,只要破坏了任意一个即可。
手动控制返回结果的顺序
根据对异步操作结果处理情况的不同也有三种不同的思路
这里先引入一个公共的 wait 函数
/** * 等待指定的时间/等待指定表达式成立 * 如果未指定等待条件则立刻执行 * 注: 此实现在 nodejs 10- 会存在宏任务与微任务的问题,切记 async-await 本质上还是 Promise 的语法糖,实际上并非真正的同步函数!!!
* 即便在浏览器,也不要依赖于这种特性。 * @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. 后面异步操作得到结果后等待前面的异步操作返回结果
/** * 将一个异步函数包装为具有时序的异步函数 * 注: 该函数会按照调用顺序依次返回结果,后面的调用的结果需要等待前面的,所以如果不关心过时的结果,请使用 {@link switchMap} 函数 * @param fn 一个普通的异步函数 * @returns 包装后的函数 */ function mergeMap(fn) { // 当前执行的异步操作 id let id = 0 // 所执行的异步操作 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 () => { // 模拟一个异步请求,接受参数并返回它,然后等待指定的时间 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) // 实际上确实执行了 3 次,结果也确实为 3 次调用参数之和 console.log(sum) })()
2. 后面异步操作得到结果后放弃前面的异步操作返回结果
是的话就直接返回后面的异步调用结果
否则将本地异步调用 id 及其结果最为[最后的]
返回这次的异步调用结果
/** * 将一个异步函数包装为具有时序的异步函数 * 注: 该函数会丢弃过期的异步操作结果,这样的话性能会稍稍提高(主要是响应比较快的结果会立刻生效而不必等待前面的响应结果) * @param fn 一个普通的异步函数 * @returns 包装后的函数 */ function switchMap(fn) { // 当前执行的异步操作 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 () => { // 模拟一个异步请求,接受参数并返回它,然后等待指定的时间 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) // 实际上确实执行了 3 次,然而结果并不是 3 次调用参数之和,因为前两次的结果均被抛弃,实际上返回了最后一次发送请求的结果 console.log(sum) })()
3. 依次处理每一个异步操作,等待上一个异步操作完成之后再执行下一个
/** * 将一个异步函数包装为具有时序的异步函数 * 注: 该函数会按照调用顺序依次返回结果,后面的执行的调用(不是调用结果)需要等待前面的,此函数适用于异步函数的内里执行也必须保证顺序时使用,
* 否则请使用 {@link mergeMap} 函数 * 注: 该函数其实相当于调用 {@code asyncLimiting(fn, {limit: 1})} 函数 * 例如即时保存文档到服务器,当然要等待上一次的请求结束才能请求下一次,不然数据库保存的数据就存在谬误了 * @param fn 一个普通的异步函数 * @returns 包装后的函数 */ function concatMap(fn) { // 当前执行的异步操作 id let id = 0 // 所执行的异步操作 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 () => { // 模拟一个异步请求,接受参数并返回它,然后等待指定的时间 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) // 实际上确实执行了 3 次,然而结果并不是 3 次调用参数之和,因为前两次的结果均被抛弃,实际上返回了最后一次发送请求的结果 console.log(sum) })()
小结
虽然三个函数看似效果都差不多,但还是有所不同的。
降低触发频率并限制异步超时时间
思考一下第二种解决方式,本质上其实是 限流 + 自动超时,首先实现这两个函数。
下面来分别实现它们
限流实现
具体实现思路可见: JavaScript 防抖和节流
/** * 函数节流 * 节流 (throttle) 让一个函数不要执行的太频繁,减少执行过快的调用,叫节流 * 类似于上面而又不同于上面的函数去抖, 包装后函数在上一次操作执行过去了最小间隔时间后会直接执行, 否则会忽略该次操作 * 与上面函数去抖的明显区别在连续操作时会按照最小间隔时间循环执行操作, 而非仅执行最后一次操作 * 注: 该函数第一次调用一定会执行,不需要担心第一次拿不到缓存值,后面的连续调用都会拿到上一次的缓存值 * 注: 返回函数结果的高阶函数需要使用 {@link Proxy} 实现,以避免原函数原型链上的信息丢失 * * @param {Number} delay 最小间隔时间,单位为 ms * @param {Function} action 真正需要执行的操作 * @return {Function} 包装后有节流功能的函数。该函数是异步的,与需要包装的函数 {@link action} 是否异步没有太大关联 */ 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 函数实际上只是为了避免一种情况,异步请求时间超过节流函数最小间隔时间导致结果返回顺序错乱。
/** * 为异步函数添加自动超时功能 * @param timeout 超时时间 * @param action 异步函数 * @returns 包装后的异步函数 */ function asyncTimeout(timeout, action) { return new Proxy(action, { apply(_, _this, args) { return Promise.race([ Reflect.apply(_, _this, args), wait(timeout).then(Promise.reject), ]) }, }) }
结合使用
测试一下
;(async () => { // 模拟一个异步请求,接受参数并返回它,然后等待指定的时间 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) // 实际上确实执行了 3 次,结果也确实为第一次次调用参数的 3 倍 console.log(sum) })()
起初吾辈因为好奇实现了这种方式,但原以为会和 concatMap
类似的函数却变成了现在这样 -更像倒置的 switchMap
了。不过由此看来这种方式的可行性并不大,毕竟,没人需要旧的数据。
总结
其实第一种实现方式属于 rxjs 早就已经走过的道路,目前被 Angular 大量采用(类比于 React 中的 Redux)。但 rxjs 实在太强大也太复杂了,对于吾辈而言,仅仅需要一只香蕉,而不需要拿着香蕉的大猩猩,以及其所处的整个森林(此处原本是被人吐槽面向对象编程的隐含环境,这里吾辈稍微藉此吐槽一下动不动就上库的开发者)。
可以看到吾辈在这里大量使用了 Proxy
,那么,原因是什么呢?这个疑问就留到下次再说吧!
关于javascript异步问题的内容就介绍到这,本文有很详细的介绍,有需要的朋友可以了解看看,希望能对大家有帮助,想要了解更多javascript异步的内容,大家可以关注群英网络其它相关文章。
文本转载自脚本之家
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
我们在学习JavaScript的时候,this关键字的知识是需要掌握的,很多新手在学习的时候,对于this关键字不是很理解,因此这篇文章就给大家深入介绍JavaScript的this关键字。
目录一.实现思路二.cavans前置准备1.获取cavans2.获取2d context对象3.绘制图片到cavans上4.获取在cavans上已绘制图片数据三.原理及实现1.黑白调2.灰色调3.反转4.复古5.红
本文给大家分享用vue怎样做拖拽排序的功能,小编觉得比较实用,因此分享给大家做个参考,实现效果及代码如下,这里提供一种简单的实现方案,感兴趣的朋友可以了解看看。
跨域是指当一个资源去访问另一个不同域名或者同域名不同端口的资源时,就会发出跨域请求,下面这篇文章主要给大家介绍了关于vue使用vite配置跨域以及环境配置的相关资料,需要的朋友可以参考下
这篇文章主要介绍了解决vue-pdf查看pdf文件及打印乱码的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008