The original question is as follows:

There are three buttons on the page, namely A, B and C. Clicking each button will send an asynchronous request without affecting each other, and the data returned by each request is the name of the button. When users click A, B, C, A, C, and B in sequence, the final data obtained is A, B, C, A, C, and B.

Subject analysis

When you got the title, you saw “asynchronous request” and you knew it had something to do with callback functions, and the ultimate solution for handling asynchronous callbacks was the Promise API. From “when the user clicks A, B, C, A, C, B, the final data is A, B, C, A, C, B.” The Promise API and the depth of understanding of asynchronous events will be tested.

First of all, we can be sure that when the user clicks the button, the time difference between making the asynchronous request and receiving the data back from the server is unpredictable, so the order in which the user clicks the button is not strongly correlated with the order in which the user receives all the data.

In other words, no matter which button is clicked, no matter whether it receives a reply or an error, the request made by each button has nothing to do with the request made by other buttons.

The static method promise.all () is not appropriate because we can’t collect all the requests at once, and if one of the requests is rejected, we can only get the rejected result.

Obviously, we need to mimic the behavior of the queue to ensure that if there is a previous request that does not receive a response (whether it succeeds or fails), the results of all subsequent requests need to be undecided until no previous requests are in progress.

Their thinking

So how do we use the Promise API to ensure that the request results behave like a queue? First we can try to customize a function with this functionality, let’s say asyncQueue(). When we execute the request (Promise instance) as an argument to asyncQueue(), we expect it to return a new Promise instance with the result:

const asyncQueue = (() = > {
  const queue = [] // Maintain an array to store the result of the request

  return function (request) {
    return new Promise((resolve, reject) = > {
      // Other operations

      request.then((res) = > resolve(res)).catch((err) = > reject(err))
    })
  }
})()
Copy the code

In IIFE, we first create an array to store the results of the request, and then return a new function that can access the array through a closure, thus avoiding the risk of contaminating global variables.

Next, we return a new Promise instance in this function, and when the incoming request is processed, the result is passed to the new instance’s handlers (resolve() and reject()) and executed through its then() and catch() methods.

By nesting a new Promise instance, we transfer processing of the requested result to that new instance, so we can decide when to change the state of that instance and deliver the result.

const asyncQueue = (() = > {
  const queue = [] // Maintains an array of callbacks that execute handler functions

  return function (promise) {
    const index = queue.length++ // Save the queue length when the function executes, and then update it

    return new Promise((resolve, reject) = > {

      // Check if there are any pending promises in the preceding state
      const checkIsLastOne = (callback) = > {
        // Triggered when the request PROMISE is in the settled state

        if (queue.length === 0) {
          // If there is no previous promise in a pending state, execute the callback directly to exit the function
          return callback()
        }

        // The pending state of the promise replaces the undefined value of index with the incoming callback
        queue.splice(index, 1, callback)

        // Loop through queue, execute all callbacks, exit loop on undefined value
        let loop = 0
        while ((fn = queue[loop++])) fn()

        if (loop === queue.length + 1) {
          When the last promise is settled, all requests will be processed and the queue will be cleared
          queue.length = 0
        }
      }

      promise
        .then((res) = > checkIsLastOne(() = > resolve(res)))
        .catch((err) = > checkIsLastOne(() = > reject(err)))
    })
  }
})()
Copy the code

When asyncQueue() is executed, the function determines whether there are any pending requests. If there are, it waits for all previous requests to complete and then passes the result of the current request.

The key to doing this is to save the length of the queue to index when asyncQueue() is executed and then update the length. When Request handles the Settled state, the result is taken out and stored temporarily as a new Promise instance handler parameter, wrapped as a callback into the checkIsLastOne() function.

In the checkIsLastOne() function, we check whether there are any previous requests in progress by checking whether the queue length is equal to 0. If so, replace the incoming callback with the element pointed to by the index in the queue array (undefined).

At this point, we do not know whether the previous request was processed, so we iterate over the elements in the queue, execute these functions in turn, stop the loop when the element value is undefine (indicating that the previous request is being processed), and empty the queue array when all the elements are iterated (all requests are being processed).

Let’s try this with asyncQueue() :

<button onclick="handleClick('A', 1000)">A</button>
<button onclick="handleClick('B', 1000)">B</button>
<button onclick="handleClick('C', 2000)">C</button>
Copy the code
const request = (name, delay) = > {
  // simulate the request
  return new Promise((resolve, reject) = > {
    setTimeout(() = > resolve(name), delay)
  })
}

const handleClick = (name, delay) = > {
  asyncQueue(request(name, delay)).then(res= > console.log(res))
}
Copy the code

Click buttons A, B, C, A, C, B in sequence and the result is as follows:

The asyncQueue() function clearly does the trick.

Promise.asyncQueue()

The asyncQueue() function solves the need to process asynchronous results in the same order as the requests made.

If the business needs to use this method heavily, we can use it as a static method on a global Promise object:

if (!Promise.asyncQueue) {
  Promise.asyncQueue = (() = > {
    const queue = [] // Maintains an array of callbacks that execute handler functions

    return function () {
      if (arguments.length === 0) {
        // A parameter must be passed
        throw new TypeError('1 argument required, but only 0 present.')}let promise = arguments[0]

      if (!/Promise/.test(Object.prototype.toString.call(promise))) {
        // Determine if the passed argument is a promise, if not promise.resolve () by default
        promise = Promise.resolve(promise)
      }

      const index = queue.length++ // Save the queue length when the function executes, and then update it

      return new Promise((resolve, reject) = > {

        // Check if there are any pending promises in the preceding state
        const checkIsLastOne = (callback) = > {
          // Triggered when the request PROMISE is in the settled state

          if (queue.length === 0) {
            // If there is no previous promise in a pending state, execute the callback directly to exit the function
            return callback()
          }

          // The pending state of the promise replaces the undefined value of index with the incoming callback
          queue.splice(index, 1, callback)

          // Loop through queue, execute all callbacks, exit loop on undefined value
          let loop = 0
          while ((fn = queue[loop++])) fn()

          if (loop === queue.length + 1) {
            When the last promise is settled, all requests will be processed and the queue will be cleared
            queue.length = 0
          }
        }

        promise
          .then((res) = > checkIsLastOne(() = > resolve(res)))
          .catch((err) = > checkIsLastOne(() = > reject(err)))
      })
    }
  })()
}
Copy the code

Promise.asyncqueue () static methods must pass in an argument, and if the argument is not of type Promise, promise.resolve () is used by default.

Then we can use it like this:

Promise.asyncQueue(asyncFn)
  .then((res) = > console.log(res))
  .catch((err) = > console.log(err))
Copy the code

summary

This article analyzes and thinks mainly from an interview question, and solves the problem of how to call asynchronous events in the same order as the function calls that process their results.

The above is the whole content of this article, my level is limited, if wrong or controversial place, please point out.

Finally, thanks for reading! Welcome to share!