JavaScript asynchronous timing problem
- 2021-09-24 21:12:31
- OfStack
Scene
After death, we will go to heaven, because we are already in hell when we live.
I don't know if you've ever had to send multiple asynchronous requests to the background, but the data displayed in the end is not correctly old data.
Details:
The user triggered the event and sent the first request The user triggered the event and sent the second request The second request was successful, and the data on the page was updated The first request was successful, and the data on the page was updatedHmm? Do you feel abnormal? This is the problem that the order of asynchronous callbacks is different from the order of invocation when multiple asynchronous requests are made.
Thinking
Why is there such a problem? How to solve this problem?Why is there such a problem?
JavaScript can be seen everywhere asynchronous, but in fact it is not so easy to control. The user interacts with UI, Trigger event and its corresponding handler function, Function performs asynchronous operation (network request), and the time (sequence) of asynchronous operation results is uncertain, so the time of response to UI is uncertain. If the event is triggered frequently/the asynchronous operation time is too long, the previous asynchronous operation results will overwrite the subsequent asynchronous operation results.
Key point
The time (order) of the results of asynchronous operations is uncertain If the event is triggered frequently/the asynchronous operation takes too longHow to solve this problem?
Since the key point consists of two elements, it is enough to destroy any one.
Manually control the order in which results are returned asynchronously Reduce the trigger frequency and limit the asynchronous timeoutManually control the order in which the results are returned
There are also three different ideas according to the different processing conditions of asynchronous operation results
Wait for the previous asynchronous operation to return the result after the following asynchronous operation gets the result Abandon the previous asynchronous operation and return the result after the subsequent asynchronous operation gets the result Process every 1 asynchronous operation in turn, and wait for the last 1 asynchronous operation to complete before executing the next 1 asynchronous operationA common wait function is introduced here
/**
* Wait for a specified time / Wait for the specified expression to hold
* Execute immediately if no wait condition is specified
* Note : This implementation is implemented in the nodejs 10- There will be problems with macro tasks and micro tasks, remember async-await Essentially still Promise Syntax sugar, in fact, is not a real synchronization function! ! ! Even in browsers, don't rely on this feature.
* @param param Waiting time / Waiting condition
* @returns Promise Object
*/
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. Wait for the previous asynchronous operation to return the result after the following asynchronous operation gets the result
Claims 1 only 1 id for every asynchronous call Record all asynchronous id using a list After the asynchronous operation is actually called, add a 1-only id Determine whether the last asynchronous operation being performed completed If not, wait for the last asynchronous operation to complete, otherwise skip it directly Remove the current id from the list Finally, wait for asynchronous operation and return results
/**
* Will 1 Asynchronous functions are wrapped as asynchronous functions with time series
* Note : This function will return results in order of call, and the results of subsequent calls need to wait for the previous ones, so if you don't care about outdated results, please use {@link switchMap} Function
* @param fn 1 Ordinary asynchronous functions
* @returns Wrapped function
*/
function mergeMap(fn) {
// Current asynchronous operation id
let id = 0
// Asynchronous operations performed id List
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
},
})
}
Test 1
;(async () => {
// Simulation 1 Asynchronous request, accepts the parameter, returns it, and waits for the specified time
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)
// In fact, it did implement 3 Times, the result is indeed 3 Sum of secondary call parameters
console.log(sum)
})()
2. Discard the previous asynchronous operation and return the result after the following asynchronous operation gets the result
Claims 1 only 1 id for every asynchronous call Record the latest id for asynchronous operation results Record the latest asynchronous operation results Execute and wait for the return result Determine whether there is a call result after this asynchronous call
If so, the result of the following asynchronous call will be returned directly
Otherwise, the local asynchronous call to id and its result are the most [last]
Returns the result of this asynchronous call
/**
* Will 1 Asynchronous functions are wrapped as asynchronous functions with time series
* Note : This function discards expired asynchronous operation results, which improves performance slightly (mainly because the results that respond quickly will take effect immediately without waiting for the previous response results)
* @param fn 1 Ordinary asynchronous functions
* @returns Wrapped function
*/
function switchMap(fn) {
// Current asynchronous operation id
let id = 0
// Finally 1 Subasynchronous operation id Operational results less than this are discarded
let last = 0
// Cache last 1 Results of sub-asynchronous operations
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
},
})
}
Test 1
;(async () => {
// Simulation 1 Asynchronous request, accepts the parameter, returns it, and waits for the specified time
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)
// In fact, it did implement 3 Times, but the result is not 3 The sum of the parameters of the first two calls, because the results of the first two calls are discarded, actually returning the last 1 The result of the second send request
console.log(sum)
})()
3. Process each asynchronous operation in turn, waiting for the last asynchronous operation to complete before executing the next one
/**
* Will 1 Asynchronous functions are wrapped as asynchronous functions with time series
* Note : This function will return the results in turn according to the calling order, and the subsequent calls (not the calling results) need to wait for the previous ones. This function is suitable for the inner execution of asynchronous functions and must be used when the order is guaranteed. Otherwise, please use {@link mergeMap} Function
* Note : This function is actually equivalent to calling {@code asyncLimiting(fn, {limit: 1})} Function
* For example, save the document to the server immediately, of course, you have to wait on 1 You can't request the following until the request ends 1 Otherwise, the data stored in the database will be fallacious
* @param fn 1 Ordinary asynchronous functions
* @returns Wrapped function
*/
function concatMap(fn) {
// Current asynchronous operation id
let id = 0
// Asynchronous operations performed id List
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
},
})
}
Test 1
;(async () => {
// Simulation 1 Asynchronous request, accepts the parameter, returns it, and waits for the specified time
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)
// In fact, it did implement 3 Times, but the result is not 3 The sum of the parameters of the first two calls, because the results of the first two calls are discarded, actually returning the last 1 The result of the second send request
console.log(sum)
})()
Summary
Although all three functions seem to have similar effects, they are still different.
Allow concurrent asynchronous operations? No: concatMap, Yes: Go to the next step Do you need to process the old results? No: switchMap, Yes: mergeMapReduce the trigger frequency and limit the asynchronous timeout
Think about the second solution under 1, which is essentially current limiting + automatic timeout. First, realize these two functions.
Limit current: Limit the frequency of function calls. If the frequency of calls is too fast, the call will not really execute but return the old value Automatic timeout: If the timeout time is reached, the function will automatically time out and throw an error even if it has not yet got a resultLet's implement them separately
Realization of current limiting
Specific implementation ideas can be seen: JavaScript anti-shake and throttling
/**
* Function throttling
* Throttling (throttle) Jean 1 Do not execute a function too frequently, reduce the execution of too fast calls, called throttling
* Function dejitter similar to but different from the above , Wrapped function on 1 The second operation will be executed directly after the minimum interval has passed , Otherwise, the operation will be ignored
* The obvious difference from the above function debounce is that the operation will be performed in a loop with the minimum interval time during continuous operation , Instead of just executing the last 1 Secondary operation
* Note : The function is the first 1 Secondary call 1 Will be implemented, don't worry about the first 1 If you can't get the cached value for the next time, you will get it for subsequent consecutive calls 1 Cached value of times
* Note : Higher-order functions that return function results need to use the {@link Proxy} Implementation to avoid information loss on the prototype chain of the original function
*
* @param {Number} delay Minimum interval time, in units ms
* @param {Function} action Actions that really need to be performed
* @return {Function} A function with throttling function after packaging. This function is asynchronous, and the function that needs to be wrapped {@link action} There is not much correlation between asynchronous and asynchronous
*/
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)
})
},
})
}
Automatic timeout
Note: The asyncTimeout function is actually only designed to avoid a situation where the asynchronous request time exceeds the minimum interval of the throttle function, resulting in out-of-order return of results.
/**
* Add automatic timeout for asynchronous functions
* @param timeout Timeout time
* @param action Asynchronous function
* @returns Wrapped asynchronous function
*/
function asyncTimeout(timeout, action) {
return new Proxy(action, {
apply(_, _this, args) {
return Promise.race([
Reflect.apply(_, _this, args),
wait(timeout).then(Promise.reject),
])
},
})
}
Combined use
Test 1
;(async () => {
// Simulation 1 Asynchronous request, accepts the parameter, returns it, and waits for the specified time
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 The result is 10 , and switchMap The difference of is that the first of the minimum interval is retained 1 While discarding the following asynchronous results, and switchMap Just the opposite!
console.log(last)
// In fact, it did implement 3 Second, the result is indeed the first 1 Object of the second call parameter 3 Times
console.log(sum)
})()
At first, we realized this way out of curiosity, but we thought it would be reconciled with
concatMap
Similar functions have become more inverted as they are now
switchMap
It's over. However, it seems that this method is not feasible. After all, no one needs old data.
Summarize
In fact, the first implementation belongs to the road that rxjs has already taken, and is widely used by Angular at present (analogy with Redux in React). However, rxjs is too powerful and complicated. For our generation, we only need one banana, not the gorilla with banana and the whole forest where it lives (this place was originally the hidden environment of object-oriented programming, and here we use this to spit out the developers who can easily go to the library).
You can see that our generation has used it a lot here
Proxy
So, what is the reason? Let's leave this question until the next time!
Above is the JavaScript asynchronous timing problem in detail, more information about JavaScript asynchronous timing please pay attention to other related articles on this site!