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 updated

Hmm? 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 long

How 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 timeout

Manually 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 operation

A 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

Claims 1 only 1 id for every asynchronous call Record all asynchronous id using a list Add 1 only 1 id to the list Determine whether the last asynchronous operation being performed completed If not, wait for the last asynchronous operation to complete, otherwise skip it directly Really invoke asynchronous operations 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 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: mergeMap

Reduce 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 result

Let'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!


Related articles: