preface

Recently, the department was in the front of recruiting. As the only front of the department, we interviewed a lot of candidates. In the interview, there was a question involving Promise:

How to control the concurrency of image requests and how to perceive whether the current asynchronous requests have been completed?

However, few of them can answer all the questions, and I feel qualified to give a callback + counting version. So let’s summarize three ways to handle asynchrony based on promises.

The example of this article is an extremely simplified comic book reader, using 4 comic book loading to introduce the implementation of different ways and differences of asynchronous processing, the following HTML code:


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Promise</title>
  <style>
    .pics{
      width: 300px;
      margin: 0 auto;
    }
    .pics img{
      display: block;
      width: 100%;
    }
    .loading{
      text-align: center;
      font-size: 14px;
      color: # 111;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="loading">Loading...</div>
    <div class="pics">
    </div>
  </div>
  <script>
  </script>
</body>
</html>
Copy the code

A single request

The simplest way to do this is to treat asynchrony one by one in a synchronous manner. Let’s start by simply implementing a thenable function that loads a single Image and a function that handles the result returned by the function.

function loadImg (url) {
  return new Promise((resolve, reject) = > {
    const img = new Image()
    img.onload = function () {
      resolve(img)
    }
    img.onerror = reject
    img.src = url
  })
}
Copy the code

The idea is to call loadImg(urls[2]) when the first loadImg(urls[1]) is complete, and then go down. If loadImg() is a synchronization function, it is natural to think of loop __ with __.

for (let i = 0; i < urls.length; i++) {
	loadImg(urls[i])
}
Copy the code

When loadImg() is asynchronous, we can only implement it with the Promise chain, resulting in a call like this:

loadImg(urls[0])
	.then(addToHtml)
	.then((a)= >loadImg(urls[1]))
	.then(addToHtml)
	/ /...
  .then((a)= >loadImg(urls[3]))
  .then(addToHtml)
Copy the code

We’ll use an intermediate variable to store the current promise, like a cursor on a linked list. The modified for loop looks like this:

let promise = Promise.resolve()
for (let i = 0; i < urls.length; i++) {
	promise = promise
				.then((a)= >loadImg(urls[i]))
				.then(addToHtml)
}
Copy the code

The Promise variable is like an iterator that constantly points to the latest returned promise, so we simplify the code further by using Reduce.

urls.reduce((promise, url) = > {
	return promise
				.then((a)= >loadImg(url))
				.then(addToHtml)
}, Promise.resolve())
Copy the code

In programming, it is possible to implement a loop statement by recursing __ from a function. So let’s change the above code to __ recursively __:

function syncLoad (index) {
  if (index >= urls.length) return 
	loadImg(urls[index])
	  .then(img= > {
		// process img
      addToHtml(img)
      syncLoad (index + 1)})}/ / call
syncLoad(0)
Copy the code

Ok, a simple asynchronous to synchronous implementation has been completed, let’s test. The simple version of this implementation is already implemented, but the top one is still loading, so how do we know the end of the recursion outside the function and hide the DOM? Promise.then() also returns the thenable function. We just need to pass the Promise chain inside syncLoad until the last function returns.

function syncLoad (index) {
  if (index >= urls.length) return Promise.resolve()
  return loadImg(urls[index])
    .then(img= > {
      addToHtml(img)
      return syncLoad (index + 1)})}/ / call
syncLoad(0)
  .then((a)= > {
	  document.querySelector('.loading').style.display = 'none'
	})
Copy the code

Now let’s refine this function to make it more generic. It takes three arguments — the asynchronous function __, the array of arguments required by the asynchronous function, and the callback function __. Failed arguments are logged and returned to the function at the end. Another thing you can think about is why the catch comes before the final then.

function syncLoad (fn, arr, handler) {
  if (typeoffn ! = ='function') throw TypeError('The first argument must be function')
  if (!Array.isArray(arr)) throw TypeError('The second argument must be an array')
  handler = typeof fn === 'function' ? handler : function () {}
  const errors = []
  return load(0)
  function load (index) {
    if (index >= arr.length) {
      return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
    }
    return fn(arr[index])
      .then(data= > {
        handler(data)
      })
      .catch(err= > {
        console.log(err)              
        errors.push(arr[index])
        return load(index + 1)
      })
      .then((a)= > {
        return load (index + 1)}}}/ / call
syncLoad(loadImg, urls, addToHtml)
  .then((a)= > {
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(console.log)
Copy the code

Demo1 Address: Single request – Multiple Promises synchronized

At this point, there are a number of non-generic problems with this function. For example, the processing function must be consistent, not a queue of different asynchronous functions, and the asynchronous callback function can only be one type. A more detailed description of this approach can be found in my previous article Koa Reference library koA-comage-Nuggets.

Of course this asynchronous to synchronous approach is not the best solution in this example, but it is a common solution when there are appropriate business scenarios.

Concurrent requests

After all, multiple HTTP requests can be made in the same domain, and promise.all is the best solution for concurrent requests that don’t need to be loaded sequentially, just processed sequentially. Because promise.all is a native function, we’ll refer to the documentation to explain.

Promise.all(iterable) means to return promises when promises in all iterable arguments have been made, or when the first Promise passed has failed. From the Promise. All () – JavaScript | MDN

Let’s change the example in Demo1:

const promises = urls.map(loadImg)
Promise.all(promises)
  .then(imgs= > {
    imgs.forEach(addToHtml)
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(err= > {
    console.error(err, 'Promise. All Reject when one of them is wrong. ')})Copy the code

Demo2 Address: concurrent request – promise.all

Concurrent requests, processing the results in order

Promise.all Multiple requests can be made, but if one Promise fails, the whole Promise is rejected. The common resource preloading in WebApp may load 20 frame-by-frame images. When there is a network problem, it is inevitable that one or two of the 20 images will fail. If the request fails, it seems inappropriate to directly discard the result returned by the other images. Just request the wrong picture again or fill it up with a placeholder. Map (loadImg), const Promises = urls.map(loadImg), and all requests for images have been sent. Let’s go through promises one by one. Start with a simpler for loop, like the single request in section 2, using the Promise chain to process it sequentially.

let task = Promise.resolve()
for (let i = 0; i < promises.length; i++) {
  task = task.then((a)= > promises[i]).then(addToHtml)
}
Copy the code

Change to reduce version

promises.reduce((task, imgPromise) = > {
  return task.then((a)= > imgPromise).then(addToHtml)
}, Promise.resolve())
Copy the code

Demo3 Address: Promise Concurrent requests are processed sequentially

Controls the maximum number of concurrent requests

Now let’s try to complete the above test. This is actually __ without controlling the maximum number of concurrent requests __. 20 pictures, loaded in two times, then use two promises. All not solved? But with Promise. All, there’s no way to listen for every image loading event. With the method in the previous section, we can both request concurrently and respond sequentially to the event that the image has finished loading.

let index = 0
const step1 = [], step2 = []

while(index < 10) {
  step1.push(loadImg(`./images/pic/${index}.jpg`))
  index += 1
}

step1.reduce((task, imgPromise, i) = > {
  return task
    .then((a)= > imgPromise)
    .then((a)= > {
      console.log(The first `${i + 1}A picture is loaded)})},Promise.resolve())
  .then((a)= > {
    console.log('>> The first 10 cards have been loaded! ')
  })
  .then((a)= > {
    while(index < 20) {
      step2.push(loadImg(`./images/pic/${index}.jpg`))
      index += 1
    }
    return step2.reduce((task, imgPromise, i) = > {
      return task
        .then((a)= > imgPromise)
        .then((a)= > {
          console.log(The first `${i + 11}A picture is loaded)})},Promise.resolve())
  })
  .then((a)= > {
    console.log('>> Next 10 sheets have been loaded ')})Copy the code

The above code is hardcode for the topic. If you can write this in the written test, it will be very good. However, no one has written it. Demo4 address (see console and network requests) : Promise loads -1 step by step

So let’s abstract the code and write a generic method that returns a Promise and continues to handle asynchronous callbacks after all images have loaded.

function stepLoad (urls, handler, stepNum) {
	const createPromises = function (now, stepNum) {
    let last = Math.min(stepNum + now, urls.length)
    return urls.slice(now, last).map(handler)
  }
  let step = Promise.resolve()
  for (let i = 0; i < urls.length; i += stepNum) {
    step = step
      .then((a)= > {
        let promises = createPromises(i, stepNum)
        return promises.reduce((task, imgPromise, index) = > {
          return task
            .then((a)= > imgPromise)
            .then((a)= > {
              console.log(The first `${index + 1 + i}A picture is loaded)})},Promise.resolve())
      })
      .then((a)= > {
        let current = Math.min(i + stepNum, urls.length)
        console.log(The total ` > >${current}Zhang has loaded! `)})}return step
}
Copy the code

The “for” in the code above can also be changed to “reduce”, but you need to first load the urls in a number of steps, into an array, interested friends can write their own. Demo5 address (see console and network request) : Promise step – 2

This implementation has nothing to do with maximum concurrency control for __. When 20 images are loaded, 10 images will be requested concurrently. When one image is loaded, another image request will be sent, keeping the number of concurrent requests at 10 until all images that need to be loaded are requested. This is a common use scenario in writing crawlers. So based on what we know above, we implement this in two ways.

Using a recursive

Assuming that our maximum number of concurrent requests is 4, the main idea of this approach is that four asynchronous Promise tasks equivalent to __ single request __ are running at the same time, and the four single requests keep recursively retrieving urls from the image URL array until all the urls are exhausted. Finally, we’ll use promise.all to handle asynchronous tasks that are still in the request. We’ll reuse the __ recursion version of section 2 to do this:

function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // Make a copy of the array
  let count = 0
  const promises = []

  const load = function () {
    if (sequence.length <= 0 || count > limit) return 
    count += 1
    console.log('Current concurrency:${count}`)
    return handler(sequence.shift())
      .catch(err= > {
        console.error(err)
      })
      .then((a)= > {
        count -= 1
        console.log('Current concurrency:${count}`)
      })
      .then((a)= > load())
  }

  for(let i = 0; i < limit && i < urls.length; i++){
    promises.push(load())
  }
  return Promise.all(promises)
}
Copy the code

Set the maximum number of requests to 5.

Promise controls maximum concurrency – Method 1

usePromise.race

Promise.race takes an array of promises and returns the value of the first Promise in that array to be resolved. Here’s the code for the Promise. Race scenario:

function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // Make a copy of the array
  let count = 0
  let promises
  const wrapHandler = function (url) {
    const promise = handler(url).then(img= > {
      return { img, index: promise }
    })
    return promise
  }
  // Maximum number of concurrent requests
  promises = sequence.splice(0, limit).map(url= > {
    return wrapHandler(url)
  })
  // limit is greater than the total number of images
  if (sequence.length <= 0) { 
    return Promise.all(promises)
  }
  return sequence.reduce((last, url) = > {
    return last.then((a)= > {
      return Promise.race(promises)
    }).catch(err= > {
      console.error(err)
    }).then((res) = > {
      let pos = promises.findIndex(item= > {
        return item == res.index
      })
      promises.splice(pos, 1)
      promises.push(wrapHandler(url))
    })
  }, Promise.resolve()).then((a)= > {
    return Promise.all(promises)
  })
}
Copy the code

Set the maximum number of requests to 5.

Promise controls the maximum number of concurrent requests – Method 2

Promise. Race: Promise. Race: Promise. Race: Promise. Race: Promise. Race: Promise. Until all urls are fetched, then use promise.all to handle callbacks when all images are finished.

Write in the last

Because THE grammar of ES6 is used extensively in my work, and await/async in Koa is the syntactic sugar of Promise, it is very important for me to understand the various flow controls of Promise. Write do not understand the place and have the wrong place welcome everyone comment correction, in addition there are other methods not involved also please provide a new way and method.

digression

At present, we have one front-end HC, base Shenzhen, the AI department of a logistics company with 50 aircraft, which requires more than three years of work experience, which is required by the company’s recruitment. If you are interested, please contact me at d2hlYXRvQGZveG1haWwuY29t

The resources

  • JavaScript Promise: introduction | Web | Google Developers
  • JavaScript Promise Mini-Book (Chinese version)