JavaScript asynchronous processing (ES6)

1, What is Promise?

Isn't it async/await? Why is Promise mentioned?

In fact, async/await is an extension of Promise, so to understand async/await better, we need to understand Promise first.

Let's see what Promise is first. First, use console.dir(Promise) in the browser to print out all the properties and methods of Promise object:

From the printed results, we can see that Promise is a constructor, which has its own methods such as all, reject, resolve, etc., and the prototype has methods such as catch, finally, then, etc. So the Promise object from new naturally has the methods of catch, finally and then. As you can see from the above figure, the then method returns a new Promise instance (note, not the original Promise instance). Therefore, you can use chain writing, that is, the then method is followed by another then method.

Promise means promise in Chinese. This * * promise to execute in the future * * object is called promise object in JavaScript. In short, it is a container that holds the results of an event (usually an asynchronous operation) that will be executed in the future.

Promise objects have two characteristics:

  1. The state of the object is not affected by the outside world.

    The project object represents an asynchronous operation with three states: pending, completed, and rejected. Only the result of the asynchronous operation can determine the current state, which cannot be changed by any other operation. This is also the origin of Promise, which means "Promise" in English, indicating that other means cannot be changed.

  2. Once the state has changed, it will not change again, and this result can be obtained at any time.

    There are only two possibilities for a Promise object to change its state:

    • From pending to fulfilled
    • From pending to rejected

    As long as these two conditions occur, the state will solidify, will not change again, and will maintain this result all the time. This is called resolved.

    If the state has changed, adding a callback function to the Promise object will get this result immediately. This is quite different from Event, which is characterized by that if you miss it and listen again, you will not get results.

Promise also has some disadvantages:

  1. Promise cannot be cancelled. Once a promise is created, it will be executed immediately and cannot be cancelled halfway;
  2. If the callback function is not set, the errors thrown inside Promise will not be reflected outside;
  3. When the Promise object is in the Pending state, it is not known which stage (just started or about to be completed) it has progressed to.

2, Use of Promise

1. Create Promise

How to create a Promise? Here is a simple example:

const p = new Promise(function(resolve, reject) {
  //Do some Async
  setTimeout(function() {
    console.log('Execution completed');
    resolve('data');
  }, 2000);
});

The Promise constructor takes a function as a parameter. The two parameters of the function are resolve and reject. These two parameters are also functions, which are provided by the JavaScript engine and need not be implemented by itself.

  • The resolve function is used to change the state of the Promise object from incomplete to successful (that is, from pending to resolved), call when the asynchronous operation is successful, and pass the result of the asynchronous operation as a parameter;
  • The reject function is used to change the state of the project object from "incomplete" to "failed" (that is, from pending to rejected), call when the asynchronous operation fails, and pass the error reported by the asynchronous operation as a parameter.

In the above code, we execute an asynchronous operation, that is, setTimeout. After 2 seconds, we output "execution complete" and call the resolve method. When we run the code, we find that we just new a Promise object, without calling it, the function we passed in has been executed.

Therefore, when we use Promise, we usually package it in a function and run it when necessary:

function runAsync() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('Execution completed');
      resolve('data');
    }, 2000);
  });
  return p;
}
runAsync();

The function return s the Promise object, that is, we get a Promise object by executing this function. At the beginning of the article, we knew that the Promise object had catch, finally, and then methods. Now let's see how to use them. Continue using the runAsync function above:

function runAsync() {
  const p = new Promise(function(resolve, reject) {
    //Do some Async
    setTimeout(function() {
      console.log('Execution completed');
      resolve('data');
      // reject('data ');
    }, 2000);
  });
  return p;
}
runAsync().then(
  function(data) {
    // success
    console.log(`Successfully get${data}`);
    //You can use the data to do other operations later
    // ......
  },
  function(error) {
    // failure
    console.log(error);
  }
);

After the project instance is generated, you can use the then method to specify the callback functions for the resolved state and rejected state respectively. If the state of the project instance changes to resolved or rejected, the callback function bound by the then method will be triggered.

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

The then method can take two callback functions as parameters. The first callback function is called when the state of the project object changes to resolved, and the second callback function is called when the state of the project object changes to rejected. The second function is optional and does not have to be provided. Both functions take as arguments the value that is passed out of the Promise object.

Conclusion: so at this time, we will find that the functions in then and our normal callback functions have the same meaning, and can be executed after the asynchronous task of runAsync is completed.

Here we can clearly know the function of Promise: in the process of asynchronous execution, separate the original callback writing method (code for executing and code for processing results), and execute the callback function in the way of chain call after the asynchronous operation is completed.

Let's take a closer look at the benefits of Promise over callback nesting.

2. Callback nesting and Promise

On the surface, Promise is only able to simplify the writing of layers of callbacks, but in essence, the essence of Promise is "state". The callback function can be called in time by maintaining state and passing state, which is much simpler and more flexible than passing callback function. Let's see what this simplification solves:

How is asynchronous code handled by nested callbacks implemented in the past?

doA(function() {
  doB();
  doC(function() {
    doD();
  });
  doE();
});
doF();

//Execution sequence:
//doA
//doF
//doB
//doC
//doE
//doD

In this way, the organized code will encounter a problem: when the code of the project becomes complex, plus various logical judgments, and constantly jump between functions, the difficulty of troubleshooting will be greatly increased. For example, in the above example, doD() can only be completed after the completion of DOC (). What if the execution of DOC () fails? Are we trying to retry doC()? Or go directly to other error handling functions? When we add these judgments to the process, the code will soon become very complex and difficult to locate the problem.

Callback nesting:

request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile('1.txt', body, function(err) {
        request(url2, function(err, res, body) {
            if (err) handleError(err)
        })
    })
});

After using Promise:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});

The benefits of using Promise are obvious.

3. catch method

The Promise object also has a catch method. What is its purpose? In fact, it is the same as the second parameter of the then method, which is used to specify the callback of reject, and the effect written in the second parameter of the then method is the same.

The usage is as follows:

runAsync()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(error){
    console.log('rejected');
    console.log(error);
});

Catch has another function: when executing the callback of resolve (the first parameter in then above), if an exception is thrown (code error), the program will not report an error and get stuck, but will enter the catch method, and the second function in then will not catch it.

Here's an example:

runAsync()
  .then(function (data) {
    console.log('resolved');
    console.log(data);
    console.log(somedata); //somedata is not defined here
  }, function (err) {
    console.log(`then The second parameter cannot catch the error${err}`);
  })
  .catch(function (error) {
    console.log('rejected');
    console.log(error);
  });

// Execution completed
// resolved
// data
// rejected
// ReferenceError: somedata is not defined

In the callback of resolve, the variable somedata is undefined. If we don't use catch, the code will directly report an error when it runs here, instead of running down. But here, you get the result. In other words, the program is executed in the catch method, and the error reason is passed to the error parameter. Even if there is error code, it will not report error, which has the same function as try/catch statement. So, if you want to catch errors, you can use the catch method.

4,Promise.all()

The Promise.all method is used to wrap multiple Promise instances into a new Promise instance.

const p = Promise.all([p1, p2, p3]);

Here's an example:

function runAsync1() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution Completion1');
      resolve('Data 1');
    }, 2000);
  });
  return p;
}

function runAsync2() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution Completion2');
      resolve('Data 2');
    }, 5000);
  });
  return p;
}

function runAsync3() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution completion3');
      resolve('Data 3');
    }, 1000);
  });
  return p;
}

Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
  console.log(results);
});

// Execution Completion1
// Execution Completion2
// Execution completion3
// ['data1', 'data2', 'data3']

Use Promise.all to execute, receive an array parameter, and the value in it will return Promise object finally. In this way, the three asynchronous operations are executed in parallel, and will not enter into then until they are all executed. So, where are the data returned by the three asynchronous operations? All are in then, Promise.all will put the results of all asynchronous operations into an array and pass them to then, that is, the results above.

Application scenario:

Promise.all method has a very common application scenario: when opening a web page, it loads all kinds of resources needed in advance, such as pictures and static files, and initializes the page after all of them are loaded.

What happens if one of the asynchronous operations contained in the Promise.all method makes an error? Let's change the resolve in runAsync1 to reject:

function runAsync1() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution Completion1');
      // Changed to reject
      reject('Data 1');
    }, 2000);
  });
  return p;
}

Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
  console.log('results' + results);
});

The program will report an error and does not enter the first parameter of the then method:

Execution completion3
//Execution Completion1
(node:25251) UnhandledPromiseRejectionWarning: Data 1
(node:25251) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:25251) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
//Execution Completion2

Add the then second parameter:

Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
  console.log('results' + results);
}, function (error) {
  console.log('error' + error);
});

// Execution completion3
// Execution Completion1
// error data 1
// Execution Completion2

Therefore, when there is an error in the asynchronous request in Promise.all(), it will not go to the function that specifies the resolved state in the then method. We need to add the function that specifies the rejected state or the catch method:

Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
  console.log('results' + results);
}).catch((err) => {
  console.log('catch' + err);
});

// Execution completion3
// Execution Completion1
// catch data 1
// Execution Completion2

That is to say, as long as one asynchronous request in project. All() is rejected, the state of project. All ([runasync1(), runasync2(), runasync3()) will become rejected.

5,Promise.race()

Race means race. Its usage is also its literal meaning: whoever runs fast will be subject to the callback. In fact, the Promise.all method is the opposite of race method. Use the example of Promise.all, but set the timeout time of runAsync1 method to 1000ms.

Promise.race([runAsync1(), runAsync2(), runAsync3()]).then(function(results) {
  console.log(results);
});

// Execution Completion1
// Data 1
// Execution Completion2
// Execution completion3

These three asynchronous operations are also executed in parallel. As a result, it can be easily guessed that runAsync1 has been executed in one second, and then the methods in then will be executed immediately. However, when the callback function in then starts to execute, runAsync2() and runAsync3() do not stop and continue to execute. So one second later, they're finished. This point needs attention.

Similarly, what happens if there is an error in the asynchronous operation in promise.race?

function runAsync1() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution Completion1');
      reject('Data 1');
    }, 1000);
  });
  return p;
}

function runAsync2() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution Completion2');
      resolve('Data 2');
    }, 2000);
  });
  return p;
}

function runAsync3() {
  const p = new Promise(function (resolve, reject) {
    //Do some Async
    setTimeout(function () {
      console.log('Execution completion3');
      resolve('Data 3');
    }, 2000);
  });
  return p;
}

Promise.race([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
  console.log(results);
}).catch((error) => {
  console.log(error);
});

// Execution Completion1
// Data 1
// Execution Completion2
// Execution completion3

That is to say, if the asynchronous request in Promise.race() is the first to change the state, the state of Promise.race([runAsync1(), runAsync2(), runAsync3())) will change accordingly and be passed to the callback function of Promise.race.

The above methods are some of the more commonly used methods of Promise.

3, Traffic light problems

Title: the red light is on once every 3 seconds, the green light is on once every 1 second, and the yellow light is on once every 2 seconds. How to keep the three lights on alternately? (implemented with Promse)

Three lighting functions already exist:

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

Using then and recursion:

function red() {
  console.log('red');
}
function green() {
  console.log('green');
}
function yellow() {
  console.log('yellow');
}

const light = function(timmer, color) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      color();
      resolve();
    }, timmer);
  });
};

const step = function() {
  Promise.resolve()
    .then(function() {
      return light(3000, red);
    })
    .then(function() {
      return light(2000, green);
    })
    .then(function() {
      return light(1000, yellow);
    })
    .then(function() {
      step();
    });
};

step();

4, Introduction to async/await

The async function is the syntax sugar of the Generator function.

Here is a Generator function that reads two files in turn:

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

function* ascReadFile() {
  yield readFile('./a.txt');
  yield readFile('./b.txt');
  yield readFile('./c.txt');
}

let g = ascReadFile();
g.next().value.then(data => {
  console.log(data.toString());
  return g.next().value;
}).then(data => {
  console.log(data.toString());
  return g.next().value;
}).then(data => {
  console.log(data.toString());
});

The function gen of the above code can be written as an async function:

const asyncReadFile = async function () {
  const f1 = await readFile('./a.txt');
  const f2 = await readFile('./b.txt');
  const f3 = await readFile('./c.txt');
  console.log(f1.toString());
  console.log(f2.toString());
  console.log(f3.toString());
};

asyncReadFile();

By comparison, you will find that the async function is to replace the asterisk (*) of the Generator function with async and the yield function with await. Compared with asterisks and yield, async and await are semantically clearer.

Syntax:

Await can only appear in async functions. Async is used to declare that a function is asynchronous, while await is used to wait for the execution of an asynchronous method to complete. Let's introduce async and await separately to help understand.

Five, async

How does the async function handle its return value? Let's write a piece of code to see what it will return:

index.js :

async function testAsync() {
  return "hello async";
}

const result = testAsync();
console.log(result);

Print results:

> node index.js
Promise { 'hello async' }

The output is a Promise object. Async function (including function statement, function expression and Lambda expression) will return a Promise object. If a direct quantity is returned in the function, async will encapsulate the direct quantity as Promise object through Promise.resolve().

Since the async function returns a Promise object, we can use the original method: then() chain to process the Promise object when the outermost layer cannot get its return value with await, as follows:

async function testAsync() {
  return "hello async";
}

testAsync().then(v => {
  console.log(v);    // hello async
});

What happens if the async function does not return a value? It's easy to think that it will return Promise.resolve(undefined).

Think about the feature of Promise - no wait, so if you execute async function without await, it will execute immediately, return a Promise object, and it will not block the following statements. This is the same as the normal function that returns the Promise object.

Six, await

The await operator is used to wait for a Promise Object. It can only be used in asynchronous functions async function Used in.

Because the async function returns a Promise object, await can be used to wait for the return value of an async function -- it can also be said that await is waiting for the async function.

But be clear, what await and so on are actually a return value -- A Promise Object or any value to wait. await is not only used to wait for Promise objects, it can wait for the result of any expression. Therefore, it can actually be followed by ordinary function calls or direct quantities.

In fact, when await is not waiting for a Promise, it has an implicit call: Promise.resolve("hello async");

function getSomething() {
  return "something";
}

async function testAsync() {
  return Promise.resolve("hello async");
}

async function test() {
  const v1 = await getSomething();
  const v2 = await testAsync();
  console.log(v1, v2);
}

test();

Await waits for what it's waiting for - a Promise object, or some other value, and then? First of all, we need to be clear: await is an operator, which is used to compose expressions. The operation results of await expressions depend on things like it.

  • If await doesn't wait for a Promise object, the result of an await expression is what it waits for.
  • If it waits for a Promise object, await is busy. It will block the following code, wait for the Promise object to resolve, and then get the value of resolve as the result of the await expression. This is why await must be used in async functions. Async function calls do not cause blocking. All the internal blocking is encapsulated in a Promise object and executed asynchronously.

7, The advantages of async/await

1, concise

Using async/await obviously saves a lot of code. We:

  • No need to write. then;
  • It is not necessary to write anonymous functions to handle the resolve value of Promise;
  • There is no need to define redundant data variables;
  • Avoid nested code.

2. Error handling

async/await makes it possible to eventually use the same code structure to handle synchronous and asynchronous errors.

In the following example with promises, if JSON.parse fails, try/catch will not be able to process because it occurs in promise. We need to call. Catch on promise and copy our error handling code, which makes the code very verbose. In actual production, it will be more complicated.

const makeRequest = () => {
  try {
    getJSON().then(result => {
      // JSON.parse may fail
      const data = JSON.parse(result);
      console.log(data);
    });
    // Uncomment, handle asynchronous code errors
    // .catch((err) => {
    //   console.log(err)
    // })
  } catch (err) {
    console.log(err);
  }
};

If we use async/await, catch can handle JSON.parse errors well:

const makeRequest = async () => {
  try {
    // JSON.parse may fail
    const data = JSON.parse(await getJSON());
    console.log(data);
  } catch (err) {
    console.log(err);
  }
};

3. Conditional statement

In the following code example, asynchronous request getJSON to get data, and then determine whether to return directly or continue to get more data according to the returned data:

const makeRequest = () => {
  return getJSON().then(data => {
    if (data.needsAnotherRequest) {
      return makeAnotherRequest(data).then(moreData => {
        console.log(moreData);
        return moreData;
      });
    } else {
      console.log(data);
      return data;
    }
  });
};

The above code will feel headache when looking at it. It has 6 layers nested. return statements can be confusing, and they just need to pass the final result to the outermost Promise. If we use async/await to rewrite the code, the readability of the code will be greatly improved:

const makeRequest = async () => {
  const data = await getJSON();
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData);
    return moreData;
  } else {
    console.log(data);
    return data;
  }
};

4. Median

In development, we often encounter such a scenario: call promise1, use the result returned by promise1 to call promise2, and then use the result of both to call promise3. Your code is likely to look like this:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return promise2(value1)
        .then(value2 => {
          // do something          
          return promise3(value1, value2);
        });
    });
};

We can make some changes to reduce nesting: put value1 and value2 into Promise.all to avoid deep nesting:

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {
      // do something          
      return promise3(value1, value2)
    })
}

The code seems to reduce nesting, but it sacrifices semantics for readability. There is no reason to put value1 and value2 in an array other than to avoid nesting.

If we use async/await, the code will become very simple and intuitive:

const makeRequest = async () => {
  const value1 = await promise1();
  const value2 = await promise2(value1);
  return promise3(value1, value2);
}

5. Error stack

Suppose there is a code that calls multiple promise in one chain, and throws an error somewhere in the chain.

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error('oops');
    });
};

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  });

The error stack returned in the Promise chain does not give a detailed reason for where the error occurred. Worse, it misleads us: the only function in the error stack is called error, but it has nothing to do with error. (file name and line number are useful, of course.).

However, the error stack in async/await points to the function where the error is located:

const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
  })

In a development environment, this may not be an advantage. However, it will be very useful when we analyze the error logs of the production environment.

6, debugging

async/await makes code debugging easier.

Because we can't set breakpoints in arrow functions that return expressions, breakpoint debugging can't be performed if Promise is used:

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error('oops');
    });
};

Also, if we set a breakpoint in the. then code block, when using the Step Over shortcut, the debugger will not jump to the next. then, it will skip asynchronous code.

If we use await/async, we will no longer need so many arrow functions, so we can skip the await statement like debugging synchronization code:

const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
}

Eight, summary

  • async/await is a new way to write asynchronous code. Previous methods include callback function and Promise.
  • async/await is implemented based on Promise and cannot be used for normal callback functions.
  • async/await, like Promise, is non blocking.
  • async/await makes asynchronous code look like synchronous code, which is clearer.
  • async/await has more advantages than Promise.

9, await in loops

Let's see how to use await correctly in the for loop.

We call a Zhihu api:

const fetch = require('node-fetch');
const bluebird = require('bluebird');

const getZhihuColumn = async (id) => {
  await bluebird.delay(1000);
  const url = `https://zhuanlan.zhihu.com/api/columns/${id}`;
  const response = await fetch(url);
  return await response.json();
};

Now there is a id list, traversing the id inside, calling getZhihuColumn in the loop:

const showColumnInfo = async () => {
  console.time('showColumnInfo');

  const names = ['feweekly', 'toolingtips'];

  for (const name of names) {
    const column = await getZhihuColumn(name);
    console.log(`Name: ${column.title} `);
    console.log(`Intro: ${column.intro} `);
  }

  console.timeEnd('showColumnInfo');
};

showColumnInfo();

Operation result:

Name: Front-end weekly 
Intro: Keep up with the pace of the times in the front-end field, with continuous improvement in breadth and depth 
Name: tooling bits 
Intro: If you want to do good work, you must first sharpen your tools 
showColumnInfo: 2446.938ms

You can see that the above writing method is also serial, just serial in the loop. How to change serial to parallel so that the code can run faster? The idea is: first trigger all requests, get an array of Promise, and then traverse the array, waiting for the results inside. The implementation is as follows:

const showColumnInfo = async () => {
  console.time('showColumnInfo');

  const names = ['feweekly', 'toolingtips'];

  const promises = names.map(name => getZhihuColumn(name));

  for (const promise of promises) {
    const column = await promise;
    console.log(`Name: ${column.title} `);
    console.log(`Intro: ${column.intro} `);
  }

  console.timeEnd('showColumnInfo');
};

showColumnInfo();

Operation result:

Name: Front-end weekly 
Intro: Keep up with the pace of the times in the front-end field, with continuous improvement in breadth and depth 
Name: tooling bits 
Intro: If you want to do good work, you must first sharpen your tools 
showColumnInfo: 1255.428ms

You can see that the running time is saved.

10, forEach's problem

1. Problem description

A few days ago, a JavaScript asynchronous problem was encountered in the project:

There is a group of data, each data needs to be processed asynchronously, and it is expected to be synchronous when processing.

The code description is as follows:

// Generate data
const getNumbers = () => {
  return Promise.resolve([1, 2, 3])
}

// Asynchronous processing
const doMulti = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (num) {
        resolve(num * num)
      } else {
        reject(new Error('num not specified'))
      }
    }, 2000)
  })
}

// Main function
const main = async () => {
  console.log('start');
  const nums = [1, 2, 3];
  nums.forEach(async (x) => {
    const res = await doMulti(x);
    console.log(res);
  });
  console.log('end');
};

// implement
main();

In this example, each number is traversed through forEach to perform a doMulti operation. The result of code execution is that start and end will be printed immediately. After 2 seconds, output 1, 4, 9 at one time.

This result is different from our expectation. We want to perform asynchronous processing every 2 seconds and output 1, 4 and 9 in turn. So the current code should be executed in parallel, and what we expect is serial execution.

We try to replace the forEach loop with the for loop:

const main = async () => {
  console.log('start');
  const nums = await getNumbers();
  for (const x of nums) {
    const res = await doMulti(x);
    console.log(res);
  }
  console.log('end');
};

The execution results are exactly as expected: start, 1, 4, 9, end.

2. Problem analysis

The way of thinking is the same, but the way of traversal is different. Why does this happen? Find the polyafill reference of forEach on MDN MDN-Array.prototype.forEach() :

// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {

  Array.prototype.forEach = function(callback, thisArg) {

    var T, k;

    if (this == null) {
      throw new TypeError(' this is null or not defined');
    }

    // 1. Let O be the result of calling toObject() passing the
    // |this| value as the argument.
    var O = Object(this);

    // 2. Let lenValue be the result of calling the Get() internal
    // method of O with the argument "length".
    // 3. Let len be toUint32(lenValue).
    var len = O.length >>> 0;

    // 4. If isCallable(callback) is false, throw a TypeError exception. 
    // See: http://es5.github.com/#x9.11
    if (typeof callback !== "function") {
      throw new TypeError(callback + ' is not a function');
    }

    // 5. If thisArg was supplied, let T be thisArg; else let
    // T be undefined.
    if (arguments.length > 1) {
      T = thisArg;
    }

    // 6. Let k be 0
    k = 0;

    // 7. Repeat, while k < len
    while (k < len) {

      var kValue;

      // a. Let Pk be ToString(k).
      //    This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty
      //    internal method of O with argument Pk.
      //    This step can be combined with c
      // c. If kPresent is true, then
      if (k in O) {

        // i. Let kValue be the result of calling the Get internal
        // method of O with argument Pk.
        kValue = O[k];

        // ii. Call the Call internal method of callback with T as
        // the this value and argument list containing kValue, k, and O.
        callback.call(T, kValue, k, O);
      }
      // d. Increase k by 1.
      k++;
    }
    // 8. return undefined
  };
}

From the setp 7 in the above polyfill, we can simply solve it into the following steps:

Array.prototype.forEach = function (callback) {
  // this represents our array
  for (let index = 0; index < this.length; index++) {
    // We call the callback for each entry
    callback(this[index], index, this);
  };
};

It is equivalent to the for loop executing the asynchronous function, so it is executed in parallel, resulting in a one-time output of all results: 1, 4, 9.

const main = async () => {
  console.log('start');
  const nums = await getNumbers();
  // nums.forEach(async (x) => {
  //   const res = await doMulti(x);
  //   console.log(res);
  // });
  for (let index = 0; index < nums.length; index++) {
    (async x => {
      const res = await doMulti(x)
      console.log(res)
    })(nums[index])
  }
  console.log('end');
};

3. Solutions

Now, we have a clear analysis of the problem. We used for of loop to replace forEach as the solution. In fact, we can also transform forEach:

const asyncForEach = async (array, callback) => {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
}

const main = async () => {
  console.log('start');
  const nums = await getNumbers();
  await asyncForEach(nums, async x => {
    const res = await doMulti(x)
    console.log(res)
  })
  console.log('end');
};

main();

4. Eslint problem

At this time, Eslint reported an error again: no-await-in-loop. The official document https://eslint.org/docs/rules/no-await-in-loop also explains this.

Good writing:

async function foo(things) {
  const results = [];
  for (const thing of things) {
    // Good: all asynchronous operations are immediately started.
    results.push(bar(thing));
  }
  // Now that all the asynchronous operations are running, here we wait until they all complete.
  return baz(await Promise.all(results));
}

Bad writing:

async function foo(things) {
  const results = [];
  for (const thing of things) {
    // Bad: each loop iteration is delayed until the entire asynchronous operation completes
    results.push(await bar(thing));
  }
  return baz(results);
}

In fact, there is no good or bad difference between the above two writing methods. The results of the two writing methods are totally different. The "good writing method" recommended by Eslint has no order when performing asynchronous operations. There is order in the "bad writing method". The specific writing method should be determined according to the business requirements.

Therefore, in When Not To Use It of the document, Eslint also mentioned that we can disable this rule if it needs to be executed in order:

In many cases the iterations of a loop are not actually independent of each-other. For example, the output of one iteration might be used as the input to another. Or, loops may be used to retry asynchronous operations that were unsuccessful. Or, loops may be used to prevent your code from sending an excessive amount of requests in parallel. In such cases it makes sense to use await within a loop and it is recommended to disable the rule via a standard ESLint disable comment.

Published 28 original articles, won praise 11, visited 2563
Private letter follow

Tags: JSON Javascript github Asterisk

Posted on Sat, 15 Feb 2020 21:39:07 -0500 by echox