About the Race Condition

There is a detailed introduction about Race Condition on Wikipedia (English version is more detailed) :

For example, there are roughly two threads modifying a global resource, ideally:

But in the absence of synchronous locks, the actual situation might look like this:

How to solve:

Most languages provide such a thing as resource locks/synchronization locks. Choose a different way to handle this problem depending on the language

At the front end of the presentation

Javascript is single-threaded and this should not happen. However, when rendering asynchronously, there will still be a timing problem with rendering, which is represented by, presumably, a detail component, watch/useEffect, passing in the ID, and then sending a request to the back end based on the ID, and then rendering asynchronously.

Because it is asynchronous, there is no guarantee that the first request will be returned first, and that the page will be displayedidInconsistencies with details:

Using the concept of a synchronous lock, you can define ablockedTo prevent subsequent requests from being sent when requested:This may seem to fix the timing of the render, but a closer look reveals that this approach leads to a new problem:

  1. The overall render cycle is much longer
  2. The front end is interactive and UI heavy, this kind of “lock” will also cause the user’s operation to be blocked, the user’s use is also bad
  3. The scenario is so one-dimensional that, for example, if you’re dealing with timing issues with input box intelligence, you can’t stop the user from typing until the previous request is returned

Request one has something to say

Real life case

Login/account switching scenario

  1. If the user does not log in, click login and click cancel before login is successful. The request is asynchronous and the cancellation is synchronous, which will result in that even if the user clicks cancel, the user is still logged in successfully.
  2. The scenario of user account switching is similar. The switch is cancelled before the switch is successful. However, the operation after the switch is successful will still be performed.

Switch to TAB/Search

Click on any letter and it returns any letter, so this one does some processing on the interface, the earlier requests were slower, when you click on it, righta -> ab -> abc, will appear: first showabc->ab->aThe same is true for searchResults:

why

There are many reasons for this timing problem, which can be summarized as follows:

  1. The current network environment is poor and unstable, which cannot ensure the stability of the request return
  2. The processing logic on the back end is different. For example, two different interfaces can trigger component updates, but the back-end processing strategy for the two interfaces is different, or the amount of data accessed by the two interfaces is different, so the request processing cycle is different, and the timing is not guaranteed
  3. The user is unlucky, and the first request is slower than the second

How to solve it

The test case

A simple Vue component, according to the input content to display different results, this piece of the interface to do processing, the first request is still the slowest response, there will be a search and results mismatch

Option 1: Start at the bottom and “cancel” the request

There are currently two types of requests: XMLHttpRequest + Fetch, the current mainstream solution is still XMLHttpRequest, Fetch is not used much because of compatibility problems, based on XMLHttpRequest, the most used is probably AXIos, this will generally cancel the request method encapsulated

We’re still going toFetchAs an example.FetchStill awkward, there are compatibility issues with request controlAbortControllerI’m going to leave that out for now. aboutAbortControllerMDN details:Follow the official example like this:

async handleSearch() {
  try {
    this.isCanceled = false;
    if (this.controller) {
      this.controller.abort();
      this.isCanceled = true;
    }
    this.controller = new AbortController();
    const { result } = await fetch(
      `http://localhost:3000/list? search=The ${this.text}`,
      {
        signal: this.controller.signal,
      }
    ).then((response) = > response.json());
    this.result = result;

    console.log("result", result);
  } catch (err) {
    console.log("err", err);
    // this.controller.signal.aborted
    if (this.isCanceled) {
      console.log("aborted");
    } else {
      this.$message("Request error"); }}}Copy the code

⚠️ Points to note:

  1. The cancellation request goes tocatch, will be coupled to some exception scenarios, so need to be handled separately
  2. This block is generating new instances every time, and I can’t find the corresponding oneresetmethods
  3. errorUnable to get cancellation request information,controller.signal.abortedAbility to determine whether a request isaborted, but because a new instance is created each time, it can only be controlled by variables

No, no, no, no, no, no, no, no, no, no.

  1. Baidu, will only keep the latest requests, previous requests will be cancelled:

2. Google, Google will keep up to 4 concurrent requests and then cancel all previous requests:

Oddly enough, none of them are shaken

Cancel the Promise

Can I get rid of the Promise? Can I get rid of the Promise? For specific cancellations, see how-to cancel-your-promise

Here are a few:

  1. Pure Promises
  2. Switch to generators
  3. Note on async/await

Written simply:

const request = (. arg) = > {
  let cancel;
  const promise = new Promise((resolve, reject) = > {
    cancel = () = > reject("aborted"); fetch(... arg).then(resolve, reject); });return [promise, cancel];
};
// ...
async handleSearch() {
  try {
    if (this.cancel) {
      this.cancel();
    }
    const [promise, cancel] = request(
      `http://localhost:3000/list? search=The ${this.text}`
    );
    this.cancel = cancel;
    const result = (await promise.then((response) = > response.json()))
      .result;
    this.result = result;

    console.log("result", result);
  } catch (err) {
    if (err === "aborted") {
      console.log(err);
    } else {
      this.$message("Request error"); }}}Copy the code

Matching the request

Only if the request currently being processed is matching, otherwise, there are two cases:

  1. There are onlykeyDistinguishing, as of a commodity:
    / / id
    async handleSearch() {
      try {
        const detail = await fetch(`xx/The ${this.id}`);
        if (detail.id === this.id) {
            this.detail = detail; }}catch (err) {
        this.$message("Request error"); }}Copy the code
  2. There is no onekey, record the endPromiseReference, and then match
    async handleSearch() {
      try {
        const curPromise = fetch(`xx/The ${this.id}`);
        this.promiseRef = curPromise;
        
        const detail = await curPromise;
        
        if (this.promiseRef === curPromise) {
            this.detail = detail; }}catch (err) {
        this.$message("Request error"); }}Copy the code

The library I used

redux-saga

Redux-saga, which I used to use with React, is a middleware component of Redux that handles side effects, namely requests. Redux-saga provides TakeLatest helper functions to handle this problem: Redux-Saga provides TakeLatest helper functions to handle this problem:

function* loadStarwarsHeroSaga() {
  yield* takeLatest(
    'LOAD_STARWARS_HERO'.function* loadStarwarsHero({ payload }) {
      try {
        const hero = yield call(fetchStarwarsHero, [
          payload.id,
        ]);
        yield put({
          type: 'LOAD_STARWARS_HERO_SUCCESS',
          hero,
        });
      } catch (err) {
        yield put({
          type: 'LOAD_STARWARS_HERO_FAILURE', err, }); }}); }Copy the code

rx-js

Rx-js is a responsive library, officially, asynchronous LoDash. Encapsulate all data into streams for processing. The main operation method used is SwitchMap:

import { Subject, merge, of } from "rxjs";
import { ajax } from "rxjs/ajax";
import { switchMap, catchError, tap } from "rxjs/operators";

export default {
  name: "HelloWorld".data() {
    return {
      text: "".result: "holder"}; },mounted() {
    this.subject = new Subject();

    this.subject
      .pipe(
        tap(() = > {
          console.log("text:".this.text);
        }),
        switchMap((str) = >
          ajax(`http://localhost:3000/list? search=The ${this.text}`)
        ),
        catchError((err, caught$) = > {
          return merge(of({ err }), caught$);
        })
      )
      .subscribe((response) = > {
        if (response.err) {
          this.$message("Request failed");
        } else {
          const result = response.response.result;
          console.log("result:", result);
          this.result = result; }}); },beforeDestroy() {
    this.subject.unsubscribe();
  },
  methods: {
    handleSearch() {
      this.subject.next(); ,}}};Copy the code

Because data is treated as a stream, timing issues are avoided:

conclusion

There are many other solutions, such as GraphQL, etc., but I don’t know enough about them. “Race” problems appeared in some simple applications of probability is relatively small, but it is easier in some complex applications, since my project to switch to the page from the B end, have never run into this problem (active page), but my friend met this problem, so is simple to sort out, probably so much, thank you for reading.