We all know that JavaScript has only one thread, and asynchrony completely avoids thread blocking and improves thread responsiveness compared to synchronous operations. At the same time, however, there is a problem: there is no guarantee that asynchronous operations will complete in the same order as they started.

To put it simply, the order in which an asynchronous operation starts does not determine the order in which it ends. A simple example is as follows:

let pro_1 = new Promise((resolve, rejct) = > {
  setTimeout((a)= > {
    resolve("pro_1");
  });
});
let pro_2 = new Promise((resolve, rejct) = > {
  resolve("pro_2");
});

pro_1.then(res= > {
  console.log(res);
});

pro_2.then(res= > {
  console.log(res);
});

// pro_2
// pro_1
Copy the code

In this case, setTimeout is used to change the order of execution, but we can call this a controlled race, because it is completely caused by the execution mechanism of JavaScript itself (not the problem). In this article, I would like to recommend two articles to learn more about JavaScript execution mechanisms (this time, understand JavaScript execution mechanisms and Tasks, microTasks, Queues and schedules).

In addition to the order of JavaScript execution mechanisms, the order in which asynchronous requests start and end is not controlled during our actual development. For example: now there is a demand, an order list query, need to directly through the TAB switch to obtain the list information.

Normally, we see screenshots like this:

Screenshot below:

Analyze the above request process:

  1. The state is initialized to A
  2. Click to switch, and the state changes to B
  3. Request the interface to get the data
  4. Asynchronous request succeeded. Data displayed

What went wrong in the process? First switch in step 2, if the initialization interface (request) is successful, the normal display, that if at the time of switch, a request is not on the return data, and the interface request, we can’t control is the last time the request to complete or to complete the request, if the last time request finally completed, Then the data we returned before will obviously be overwritten, causing data confusion.

Let’s look at another requirement: in the input box, we can add association function to make API interface request in the process of user input. Similarly, we can add anti-shake or throttling method to make asynchronous request in the process of input, but we still cannot guarantee that the returned result corresponds to the input content.

Two problems can be found from the above two requirements: frequent asynchronous requests and failure to ensure that the return result corresponds to the previous status after a successful request. We can discuss in two directions:

  • Avoid multiple requests
  • The status before and after the request is associated

Avoid multiple requests

In the asynchronous request process with the server, some user operations frequently request resources, which may cause certain impact. To avoid multiple requests, you can do the following:

Scenario 1. Commit in a synchronous manner, and make the next request after the current request completes (successfully or failed)
if (this.pendding) return 

this.pendding = true

api().then(res= > {
    this.pendding = false
}).catch(error= > {
    this.pendding = false
})
Copy the code
Scenario 2. Abort all previous requests based on the last request
if (this.pendding) {
    this.ajax.abort()
}
this.pendding = true
this.ajax = $.ajax({})
Copy the code

Reading this, you may be wondering: How do you abort an ongoing request? We can introduce several schemes according to the requirements of different situations:

  • Abort native XMLHttpRequest
let xhr = new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";
xhr.open(method,url,true);

xhr.send();

xhr.abort();
Copy the code
  • abort jQuery
let ajax = $.ajax({})
...
ajax.abort()
Copy the code
  • Abort Axios request in progress

We all know that AXIos is wrapped around promises, so think again: How do you abort promises?

Start by creating an example of a promise:

let promise = new Promise((resolve, reject) = > {
  resolve('success')
})
promise.then(res= > {
  console.log(res, 'then_1')
  return res
}).then(res= > {
  console.log(res, 'then_2')
}).catch(error= > {
  console.error(error)
})
Copy the code

Now, if we want to stop the output of then_2, what do we do?

(1). Thro w or promise.reject ()

promise.then(res= > {
  console.log(res, 'then_1')
  // throw new Error(' abort current promise')
  return Promise.reject({error: 'Abort current Promise'})
}).then(res= > {
  console.log(res, 'then_2')
}).catch(error= > {
  console.error(error)
})
Copy the code

At this time, it is found that the errors actively thrown by the system cannot be distinguished from the errors reported by the system, so it is necessary to mark the errors actively thrown by the system.

promise.then(res= > {
  console.log(res, 'then_1')
  // let e = new Error()
  // e.name = 'isInitiativeError'
  // e.message = true
  // throw e
  return Promise.reject({message: 'Abort current Promise'.isInitiativeError: true})
}).then(res= > {
  console.log(res, 'then_2')
}).catch(error= > {
  if (error.isInitiativeError) {
    console.warn('Abort voluntarily! ')
    return
  }
  if (error.name == 'isInitiativeError' && error.message) {
    console.warn('Abort voluntarily! ')
    return
  }
  console.error(error)
})
Copy the code

We can skip the then and the first catch, but there is no way to abort the then (or throw or promise.reject () in the catch). Make sure that the catch continues into the next catch, which is also guaranteed to terminate the then, and then can be solved by the second method.

(2). Return new Promise() to ensure that the operation cannot continue by keeping the Promise’s pending state;

promise.then(res= > {
  console.log(res, 'then_1')
  return new Promise((resolve, reject) = > {
    console.log('Make a promise on the way')
  })
}).then(res= > {
  console.log(res, 'then_2')
}).catch(error= > {
  if (error.isInitiativeError) {
    console.warn('Abort voluntarily! ')
    return
  }
  if (error.name == 'isInitiativeError' && error.message) {
    console.warn('Abort voluntarily! ')
    return
  }
  console.error(error)
}).then(res= > {
  console.log('then_3')})Copy the code

After the callback function ends, the promise releases the function reference; However, if a promise remains pending, the memory of the callback function cannot be freed, causing a memory leak. (In the search for a perfect solution…)

Knowing how to abort a promise, how can abort make an AXIos request? A search of Axios’ documentation shows that it provides a cancellation API (cancelling requests using the Cancel Token), and Axios’s Cancel Token API is based on cancelable Promises Proposal

The cancel token can be created using the canceltoken. source factory method like this:

var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // Processing error}});// Cancel the request (the message argument is optional)
source.cancel('Operation canceled by the user.');
Copy the code

CancelToken can also be created by passing an executor function to the CancelToken constructor:

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // The executor function takes a cancel function as an argumentcancel = c; })});// Cancel the request
cancel();
Copy the code

Note: Multiple requests can be cancelled using the same Cancel token

  • Abort fetch The sent request

AbortController AbortController is used to abort ongoing requests. AbortController supports abort requests.

let  controller = null, signal = null
// If yes, the request is interrupted
if (controller) {
  controller.abort()
}
if (AbortController) {
  controller = new AbortController();
  signal = controller.signal;
}
api().then((a)= >{... })Copy the code

The status before and after the request is associated

With the abort request interface mentioned above, we can then associate the pre-request state with the return result to ensure the correct presentation of the information. The status of the asynchronous request is recorded first, and the status is checked after the asynchronous request is completed.

getList () {
  this.loading = true
  // Record the status
  let _id = this.id

  api().then((a)= >{
    // If the current state is not the same as the record state, return directly
    if(_id ! =this.id) return. })}Copy the code

Bonus: Watch cleanup side effects in VUE 3.0

watch(idValue, (id, oldId, onCleanup) => {
  const token = performAsyncOperation(id)
  onCleanup((a)= > {
    // The id has changed or Watcher is about to be stopped.
    // Cancel asynchronous operations that have not yet completed.
    token.cancel()
  })
})
Copy the code

Vue function-based API RFC, the third parameter that the watch callback receives in the new API is a Function that registers the cleanup operation. That is, an asynchronous operation changes the data before it completes, and we may have to undo the previous operation that we are still waiting on. Huh??????? This is not exactly similar to the requirements we mentioned above, and you can look for a better solution from the UNIVERSITY of Utah article.

Through the discussion of the above two directions, we find that both schemes can avoid the occurrence of data confusion. The two schemes can not only be used under such requirements, but also can be adjusted to avoid multiple clicks to submit, multiple downloads and other requirements. Of course this is an optimization point. If there are any mistakes in this article, please correct them, thank you!!