Various operations requested by the front-end API

Time:2021-8-8

Welcome to my official account.Rui talk, get my latest articles:
Various operations requested by the front-end API

1、 Foreword

API request control has always been a hot issue in the front-end field. There are many excellent open source projects available on the market. In the spirit of teaching people to fish, this text introduces how to use the most simple code to solve practical problems in various scenarios without all tool functions.

2、 Concurrency control

In some scenarios, the front end needs to send a large number of network requests in a short time without occupying too many system resources, which requires concurrency control of requests. The request here may be the same interface or multiple interfaces. Generally, it will be processed uniformly after all interfaces are returned. In order to improve efficiency, we want to empty the location immediately after a request is completed, and then launch a new request. Here we can use it comprehensivelyPromiseThe two tools and methods to achieve the goal areraceandall

async function concurrentControl(poolLimit, requestPool) {
  //Store the promise returned by all requests
  const ret = [];
  //An executing request to control concurrency
  const executing = [];

  while (requestPool.length > 0) {
    const request = requestPool.shift();
    const p = Promise.resolve().then(() => request());
    ret.push(p);
    //P. then () returns a new promise indicating the status of the current request
    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
    executing.push(e);
    if (executing.length >= poolLimit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(ret);
}

This line of code is key:
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
To understand this line of code correctly, you must understandpromiseIt has the following characteristics:

  • p. The return value of then () is apromise, the then function executes code synchronously
  • p. The function of then () is topthispromiseSubscribe, similar todomYesaddEventListener
  • FN in then (FN) has to wait untilpromiseAfter resolve, it will be asynchronously executed by the JS engine in the micro task queue

Therefore, the real execution order of the above code is:

const e = p.then(fn);
executing.push(e);
//P execute FN after resolve
() => executing.splice(executing.indexOf(e), 1)

The following is the test code, which can be verified by yourself if you are interested.

let i = 0;
function generateRequest() {
  const j = ++i;
  return function request() {
    return new Promise(resolve => {
      console.log(`r${j}...`);
      setTimeout(() => {
        resolve(`r${j}`);
      }, 1000 * j);
    })
  }
}
const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()];

async function main() {
  const results = await concurrentControl(2, requestPool);
  console.log(results);
}
main();

Used in the previous implementationasync/awaityesES7Characteristics of, usingES6The same effect can be achieved.

function concurrentControl(poolLimit, requestPool) {
  //Store the promise returned by all requests
  const ret = [];
  //An executing request to control concurrency
  const executing = [];

  function enqueue() {
    const request = requestPool.shift();
    if (!request) {
      return Promise.resolve();
    }
    const p = Promise.resolve().then(() => request());
    ret.push(p);

    let r = Promise.resolve();
    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
    executing.push(e);
    if (executing.length >= poolLimit) {
      r = Promise.race(executing);
    }

    return r.then(() => enqueue());
  }

  return enqueue().then(() => Promise.all(ret));
}

The method of nested function calls is used here, and the code does not need to be implementedasync/awaitThe method is concise, but it has another advantage. It supports dynamic addition of new requests:

const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()];
function main() {
  concurrentControl(2, requestPool).then(results => console.log(results));
  //Dynamically add new requests
  requestPool.push(generateRequest());
}

As can be seen from the code, we can dynamically add new requests to the requestpool before the request is completed, which is suitable for some scenarios where requests are initiated according to conditions.

3、 Throttle control

The traditional throttling is to control the timing of request sending, while the throttling mentioned in this paper is to reuse the results of requests through the design pattern of publish and subscribe, which is suitable for the scenario of sending multiple identical requests in a short time. The code is as follows:

function generateRequest() {
  let ongoing = false;
  const listeners = [];

  return function request() {
    if (!ongoing) {
      ongoing = true
      return new Promise(resolve => {
        console.log('requesting...');

        setTimeout(() => {
          const result = 'success';
          resolve(result);
          ongoing = false;

          if (listeners.length <= 0) return;

          while (listeners.length > 0) {
            const listener = listeners.shift();
            listener && listener.resolve(result);
          }
        }, 1000);
      })
    }

    return new Promise((resolve, reject) => {
      listeners.push({ resolve, reject })
    })
  }
}

The key point here is to create a new request if there is an ongoing requestpromise, willresolveandrejectThe result of the subscription request is stored in the listeners array.

The test code is as follows:

const request = generateRequest();

request().then(data => console.log(`invoke1 ${data}`));
request().then(data => console.log(`invoke2 ${data}`));
request().then(data => console.log(`invoke3 ${data}`));

3、 Cancel request

There are two ways to implement a cancellation request. Let’s look at the first.
The validity of the request is controlled by setting a flag, which is combined belowReact HooksTo explain.

useEffect(() => {
  //Validity identification
  let didCancel = false;
  const fetchData = async () => {
    const result = await getData(query);
    //Judge validity before updating data
    if (!didCancel) {
      setResult(result);
    }
  }
  fetchData();
  return () => {
    //The setting data is invalid when the query is changed
    didCancel = true;
  }
}, [query]);

After the request returns, first judge the validity of the request. If it is invalid, ignore the subsequent operations.

The above implementation method is not really cancel, but rather discard. If you want to implement a real cancellation request, you need to useAbortControllerAPI, the example code is as follows:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
}).catch(err => {
  if (err.name === 'AbortError') {
    console.log('Fetch aborted');
  } else {
    console.error('Uh oh, an error!', err);
  }
});

When calledabort()When,promiseWill be rejected, triggering aAbortErrorYesDOMException

4、 Elimination request

In a scenario like the search box, the user needs to prompt search suggestions while entering, which requires sending multiple requests in a short time, and the results of the previous requests cannot cover the later ones (network congestion may cause the first requests to be sent and then returned). Obsolete requirements can be eliminated in the following way.

//Request serial number
let seqenceId = 0;
//Sequence number of the last valid request
let lastId = 0;

function App() {
  const [query, setQuery] = useState('react');
  const [result, setResult] = useState();

  useEffect(() => {
    const fetchData = async () => {
      //When a request is initiated, the sequence number is incremented by 1
      const curId = ++seqenceId;
      const result = await getData(query);
      //Only data whose serial number is larger than the last valid serial number will be displayed
      if (curId > lastId) {
        setResult(result); 
        lastId = curId;
      } else {
        console.log(`discard ${result}`); 
      
    fetchData();
  }, [query]);

  return (
    ...
  );
}

The key point here is to compare whether the sequence number of the request is larger than the last valid request when the request is returned. If not, it indicates that a subsequent request responded first, and the current request should be discarded.

5、 Summary

This paper lists several special scenarios when the front end processes API requests, including concurrency control, throttling, cancellation and elimination, and summarizes the solutions according to the characteristics of each scenario, which improves the performance while ensuring the data effectiveness.