死后我们必升天堂,因为活时我们已在地狱。
不知你是否遇到过,向后台发送了多次异步请求,结果最后显示的数据却并不正确 -- 是旧的数据。
具体情况:
嗯?是不是感觉到异常了?这便是多次异步请求时会遇到的异步回调顺序与调用顺序不同的问题。
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()
}
})
}
/**
* 将一个异步函数包装为具有时序的异步函数
* 注: 该函数会按照调用顺序依次返回结果,后面的调用的结果需要等待前面的,所以如果不关心过时的结果,请使用 {@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)
})()
/**
* 将一个异步函数包装为具有时序的异步函数
* 注: 该函数会丢弃过期的异步操作结果,这样的话性能会稍稍提高(主要是响应比较快的结果会立刻生效而不必等待前面的响应结果)
* @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)
})()
/**
* 将一个异步函数包装为具有时序的异步函数
* 注: 该函数会按照调用顺序依次返回结果,后面的执行的调用(不是调用结果)需要等待前面的,此函数适用于异步函数的内里执行也必须保证顺序时使用,否则请使用 {@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 () => {
let last = 0
let sum = 0
// 模拟一个异步请求,接受参数并返回它,然后等待指定的时间
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 结果为 10,和 switchMap 的不同点在于会保留最小间隔期间的第一次,而抛弃掉后面的异步结果,和 switchMap 正好相反!
console.log(last)
// 实际上确实执行了 3 次,结果也确实为第一次次调用参数的 3 倍
console.log(sum)
})()
起初吾辈因为好奇实现了这种方式,但原以为会和 concatMap 类似的函数却变成了现在这样 -- 更像倒置的 switchMap 了。不过由此看来这种方式的可行性并不大,毕竟,没人需要旧的数据。
其实第一种实现方式属于 rxjs 早就已经走过的道路,目前被 angular 大量采用(类比于 react 中的 Redux)。但 rxjs 实在太强大也太复杂了,对于吾辈而言,仅仅需要一只香蕉,而不需要拿着香蕉的大猩猩,以及其所处的整个森林(此处原本是被人吐槽面向对象编程的隐含环境,这里吾辈稍微藉此吐槽一下动不动就上库的开发者)。
原文来自:https://blog.rxliuli.com/p/debc4f0e/
javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。
如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它
Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性
回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象
JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中
传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)
JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。
请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。
将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!