You said you would Promise? Can you solve these five problems in the project?

Keywords: Javascript Front-end ECMAScript Interview Promise

preface

Hello, I'm Lin Sanxin. In the most easy to understand words, the most difficult knowledge point is my motto. The foundation is the advanced premise and my initial intention. As we all know, Promise is very important in our development. I think the use level of Promise can be divided into three levels

  • 1. Master the basic use of Promise
  • 2. Master the basic principle of Promise
  • 3. Be able to flexibly use Promise to solve some problems in the project

The first point is to master some basic usage methods and methods of Promise, such as then, catch, all, race, finally, allSettled, any, resolve, etc

The second point is to simply implement the principle of Promise, which can make us have a better understanding of the common methods of Promise

The third point is to flexibly Promise to solve some problems in our development. Today I'll tell you what problems I solved in project development with Promise!

Interface request timeout

As the name suggests, if the interface request exceeds a given time, an error will be reported

1. Self realization

The implementation idea is to race the interface request with the delay function and use a Promise package. Since the Promise state is irreversible, if the interface request runs first, it means that it has not timed out and the Promise state is fully. On the contrary, if the delay function runs first, it means that it has expired and the Promise state is rejected, Finally, judge whether there is timeout according to the Promise status

/**
 * Analog delay
 * @param {number} delay delay time 
 * @returns {Promise<any>}
 */
function sleep(delay) {
  return new Promise((_, reject) => {
    setTimeout(() => reject('Timeout'), delay)
  })
}

/**
 * Simulation request
 */
function request() {
  // Suppose the request takes 1s
  return new Promise(resolve => {
    setTimeout(() => resolve('Success'), 1000)
  })
}

/**
 * Determine whether the timeout occurs
 * @param {() => Promise<any>} requestFn Request function
 * @param {number} delay Delay duration
 * @returns {Promise<any>}
 */
function timeoutPromise(requestFn, delay) {
  return new Promise((resolve, reject) => {
    const promises = [requestFn(), sleep(delay)]
    for (const promise of promises) {
      // If timeout occurs, the execution fails; if no timeout occurs, the execution succeeds
      promise.then(res => resolve(res), err => reject(err))
    }
  })
}

2,Promise.race

In fact, the code in timeoutPromise can be replaced by Promise.race, which has the same effect

function timeoutPromise(requestFn, delay) {
   // If the delay Promise is returned first, it indicates that it has timed out
   return Promise.race([requestFn(), sleep(delay)])
}

3. Testing

// overtime
timeoutPromise(request, 500).catch(err => console.log(err)) // Timeout

// No timeout
timeoutPromise(request, 2000).then(res => console.log(res)) // Success

Turntable lottery

When we draw the lottery on the turntable, we usually start to rotate and also initiate the interface request, so there are two possibilities

  • 1. It's not normal that the interface hasn't requested to come back after the turntable has been turned
  • 2. It is normal for the interface to complete the request before the turntable is completed, but it is necessary to ensure that the request callback and the turntable completion callback are executed at the same time

1. After the turntable is turned, the interface hasn't requested to come back

The main problem is how to judge whether the interface request time exceeds the time required for the turntable to rotate. In fact, we can use the previous knowledge point. The interface request timeout is the same. If the time required for the turntable to complete rotation is 2500ms, we can limit that the interface request needs to come back 1000ms in advance, that is, the timeout of the interface request is 2500ms - 1000ms = 1500ms

/**
 * Analog delay
 * @param {number} delay delay time 
 * @returns {Promise<any>}
 */
function sleep(delay) {
  return new Promise((_, reject) => {
    setTimeout(() => reject('Timeout'), delay)
  })
}

/**
 * Simulation request
 */
function request() {
  return new Promise(resolve => {
    setTimeout(() => resolve('Success'), 1000)
  })
}

/**
 * Determine whether the timeout occurs
 * @param {() => Promise<any>} requestFn Request function
 * @param {number} delay Delay duration
 * @returns {Promise<any>}
 */
function timeoutPromise(requestFn, delay) {
   return Promise.race([requestFn(), sleep(delay)])
}

2. Before the rotary table is turned, the interface request is completed

We have ensured that the interface request can be requested back before the turntable is turned. However, another problem is to ensure that the request callback and the turntable turn back callback are executed at the same time, because although the turntable is still turning when the interface request comes back, we need to execute the two callbacks together when the turntable is turned back

Hearing this description, I believe many students will think of Promise.all

// ... code above

/**
 * Simulate the delay from rotary table rotation to stop
 * @param {number} delay delay time 
 * @returns {Promise<any>}
 */
 function turntableSleep(delay) {
  return new Promise(resolve => {
    setTimeout(() => resolve('Stop turning'), delay)
  })
}

/**
 * Determine whether the timeout occurs
 * @param {() => Promise<any>} requestFn Request function
 * @param {number} turntableDelay How long does the turntable turn
 * @param {number} delay Request timeout length
 * @returns {Promise<any>}
 */

function zhuanpanPromise(requsetFn, turntableDelay, delay) {
  return Promise.all([timeoutPromise(requsetFn, delay), turntableSleep(turntableDelay)])
}

3. Testing

// It does not time out and requests data back before the turntable stops
zhuanpanPromise(request, 2500, 1500).then(res => console.log(res), err => console.log(err))

Scheduler to control concurrent Promise

Imagine that one day you suddenly send 10 requests at one time, but in this case, the concurrency is very large. Can you control it, that is, only send 2 requests at a time. When a request is completed, let the third fill in, then the request is completed, and then let the fourth fill in, and so on, so as to make the maximum concurrency controllable

addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
The output order is: 2 3 1 4

Complete execution process of the whole:

At the beginning, 1 and 2 tasks are executed
500ms When, task 2 is completed, output 2, and task 3 starts to execute
800ms When, task 3 is completed, output 3, and task 4 begins to execute
1000ms When 1 task is executed, 1 is output, and only 4 tasks are left to execute
1200ms When the task is completed, output 4

realization

class Scheduler {
  constructor(limit) {
    this.queue = []
    this.limit = limit
    this.count = 0
  }
  

  add(time, order) {
    const promiseCreator = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log(order)
          resolve()
        }, time)
      })
    }
    this.queue.push(promiseCreator)
  }

  taskStart() {
    for(let i = 0; i < this.limit; i++) {
      this.request()
    }
  }

  request() {
    if (!this.queue.length || this.count >= this.limit) return
    this.count++
    this.queue.shift()().then(() => {
      this.count--
      this.request()
    })
  }
}

test

// test
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
  scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();

Cancel duplicate request

For example, when we submit a form, in order to prevent repeated submission, we will add anti shake measures to the click event of the button. This really effectively avoids the repeated requests caused by multiple clicks, but it still has disadvantages

As we all know, for better user experience, the delay of anti shake can not be too long. Generally, in my project, it is 300ms, but this can only control the interface request with request time < 300ms. If an interface request needs 2000ms, anti shake can not completely limit repeated requests at this time, So we need to do something extra to cancel the duplicate request

realization

Implementation idea: in short, use Promise.race method to install a mine around each request. If the first request is followed by a second repeated request, execute the mine around the first request, blow up the first request, and so on.

class CancelablePromise {
  constructor() {
    this.pendingPromise = null
    this.reject = null
  }

  request(requestFn) {
    if (this.pendingPromise) {
      this.cancel('Cancel duplicate request')
    }

    const promise = new Promise((_, reject) => (this.reject = reject))
    this.pendingPromise = Promise.race([requestFn(), promise])
    return this.pendingPromise
  }

  cancel(reason) {
    this.reject(reason)
    this.pendingPromise = null
  }
}

function request(delay) {
  return () => 
    new Promise(resolve => {
      setTimeout(() => {
        resolve('The final winner is me')
      }, delay)
    })
}

test

const cancelPromise = new CancelablePromise()

// Simulate frequent requests 5 times
for (let i = 0; i < 5; i++) {
  cancelPromise
    .request(request(2000))
    .then((res) => console.log(res)) // The last winner is me
    .catch((err) => console.error(err)); // First four de duplication requests
}

Global request loading

For example, a page or multiple components need to request and display the loading status. At this time, we don't want each page or component to write loading, so we can manage loading uniformly. There are two cases of loading

  • 1. loading is displayed as long as one of the global interfaces is still in the request
  • 2. If all global interfaces are not in the request, loading is hidden

How can we know the request status of the global interface? In fact, we can use Promise. As long as an interface requests that Promise's status is not pending, it means that his request is completed. Whether the request succeeds or fails, since it succeeds or fails, we will think of Promise.prototype.finally

realization

class PromiseManager {
  constructor() {
    this.pendingPromise = new Set()
    this.loading = false
  }

  generateKey() {
    return `${new Date().getTime()}-${parseInt(Math.random() * 1000)}`
  }

  push(...requestFns) {
    for (const requestFn of requestFns) {
      const key = this.generateKey()
      this.pendingPromise.add(key)
      requestFn().finally(() => {
        this.pendingPromise.delete(key)
        this.loading = this.pendingPromise.size !== 0
      })
    }
  }
}

test

// Simulation request
function request(delay) {
  return () => {
    return new Promise(resolve => {
      setTimeout(() => resolve('Success'), delay)
    })
  }
}

const manager = new PromiseManager()

manager.push(request(1000), request(2000), request(800), request(2000), request(1500))

const timer = setInterval(() => {
   // Polling to view loading status
   console.log(manager.loading)
}, 300)

reference resources

epilogue

If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha. Or join my group. Haha, let's fish and learn together: meron857287645

Posted by koglakci on Fri, 26 Nov 2021 11:03:27 -0800