03/18-20:36 Updated some more specific problem descriptions

I would like to discuss with you a problem of anti-shake in the recent project.

Issue an overview

The general requirement is that you have a table, and clicking on any row loads some details about it (on the same page as the table). The load step is a Promise chain that gets information from two different servers in turn (there are dependencies that cannot send requests at the same time).

In the case of multiple clicks within a short period of time, the loading time is different each time, which may result in the final display of the content not the last click, and each click will have DOM manipulation, resulting in the loss of browser performance.

We thought it was reasonable to prevent users from clicking further while loading. However, this was rejected by the customer: users should not be restricted freedom, and if they click wrong, do they have to wait until loading is complete to change

At this point, we naturally thought of using anti-shake for delayed execution. The problem is that the loading time is so wide (anywhere from a few hundred milliseconds to a few seconds) that traditional anti-shaking doesn’t work in this case.

For example, if we delay execution by 500 milliseconds, the first click takes 2 seconds to load, and then we click load again a second later, this time in 500 milliseconds, the result is that the next result is displayed first and then overwritten by the previous result. If we set the delay value too high, it will greatly reduce the user experience.

This brings us to today’s topic, how to make DOM changes with only one last click before the Promise chain gets the final result. P.S. Due to the complexity of the actual project, HTTP requests are encapsulated in other modules, so abort is not considered here to achieve better optimization.

The following is a simplified version of the actual problem: P1, P2, p3 form the Promise chain, as you can see, each click will perform the change page. (Fixed the Promise execution time, and added one Promise to better extend the case assuming n promises)

const p1 = (data) = > {
  return new Promise(resolve= > {
    setTimeout((a)= > resolve(data + 1), 200);
  });
};
const p2 = (data) = > {
  return new Promise(resolve= > {
    setTimeout((a)= > resolve(data + 2), 300);
  });
};
const p3 = (data) = > {
  return new Promise(resolve= > {
    setTimeout((a)= > resolve(data + 3), 500);
  });
};
const onClick = (data) = > {
  p1(data)
    .then(data= > p2(data))
    .then(data= > p3(data))
    .then(result= > {
      // Change the page for the return value of the operation
      console.log(result);
    })
    .catch(err= > {
      // Processing error
    });
};
// Simulate clicking
onClick(1);
setTimeout((a)= > onClick(2), 400);
setTimeout((a)= > onClick(3), 2000);
/ / 7
/ / 8
/ / 9
Copy the code

Plan a

We can set a counter on onClick, increment by 1 each time we click, and only change the page if the current value matches counter.

// omit p1,p2,p3 declaration
let counter = 0;
const onClick = (data) = > {
  const current = ++counter;
  p1(data)
    .then(data= > p2(data))
    .then(data= > p3(data))
    .then(result= > {
      if (current === counter) {
        // Change the page for the return value of the operation
        console.log(result);
      }
    })
    .catch(err= > {
      if (current === counter) {
        // Processing error}}); }; onClick(1);
setTimeout((a)= > onClick(2), 400);
setTimeout((a)= > onClick(3), 2000);
// The first onClick does not refresh the page
/ / 8
// 9 The third click has already refreshed the second page, so the third click continues to refresh the page
Copy the code

This solution basically solves the problem, but if you think about it, all of the Promise chains are actually executed on every click. For example, on the second onClick, the first Promise chain only executes to P2, so can we not perform P3 to achieve better optimization?

Plan 2: Further optimize on the basis of Plan 1

Further optimization is achieved by nesting a function on each Promise, directly rejecting the Promise chain if a counter does not match.

// omit p1,p2,p3 declaration
let counter = 0;
const onClick = (data) = > {
  const current = ++counter;
  p1(data)
    .then(wrapWithCancel(p2))
    .then(wrapWithCancel(p3))
    .then(result= > {
      if (current === counter) {
        // Refresh the page for the return value of the operation
        console.log(result);
      }
    })
    .catch(err= > {
      if(current === counter && err ! = ='cancelled') {
        Handle errors other than cancelled}});function wrapWithCancel(fn) {
    return (data) = > {
      if (current === counter) {
        return fn(data);
      } else {
        return Promise.reject('cancelled'); }}}}; onClick(1);
setTimeout((a)= > onClick(2), 100);
setTimeout((a)= > onClick(3), 400);
// neither p2 nor p3 of the first onClick will be executed
// p3 of the second onClick will not be executed
/ / 9
Copy the code

Plan three: add the conventional anti – shake delay execution

We can also add the conventional anti-shake delay execution on this basis to further optimize:

// omit p1,p2,p3 declaration
let counter = 0;
const onClick = (data) = > {
  const current = ++counter;
  p1(data)
    .then(wrapWithCancel(p2))
    .then(wrapWithCancel(p3))
    .then(result= > {
      if (current === counter) {
        // Refresh the page for the return value of the operation
        console.log(result);
      }
    })
    .catch(err= > {
      if(current === counter && err ! = ='cancelled') {
        Handle errors other than cancelled}});function wrapWithCancel(fn) {
    return (data) = > {
      if (current === counter) {
        return fn(data);
      } else {
        return Promise.reject('cancelled'); }}}};const debounce = function (fn, wait) {
  var timer = null;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout((a)= >{ fn.apply(context, args); }, wait); }};const debounced = debounce(onClick, 200);
debounced(1);
setTimeout((a)= > debounced(2), 100);
setTimeout((a)= > debounced(3), 200);
setTimeout((a)= > debounced(4), 600);
// The first two onClick elements p1,p2 and p3 will not be executed
// p3 of the third onClick will not be executed
/ / 10
Copy the code

For the first time, please feel free to point out your mistakes. If you have a better way, we also hope to discuss and make progress together