preface

If you are interested in the mechanism of synchronous asynchronous tasks, please read my previous article “EventLoop and Microtask Macro Tasks from browser Principles”. If this article is helpful to you, please collect, like, comment, reprint please indicate the source.

One, synchronous and asynchronous

1. The synchronous

Let’s start with a scenario: your company is about to give out year-end bonuses!!

Your boss and the accountant of your company have a booth, all the employees go to the line to get the money, you are the first, the accountant starts to calculate your “attendance”, “performance” this year, and finally tell the amount you deserve to the boss, the boss put the money in your hand, you are happy to take the money to go. The next colleague came, the accountant began to calculate, the boss gave the money; Just one by one.

This is classic synchro, where the colleague at the back of the line has to wait for the one before him to get paid. The problem is that it’s too long to wait for all the employees to get their annual bonuses. If you run into a “tough” person who doesn’t have enough money to pay for it, you’ll have to prepare for the rest of your coworkers to spend the night in the office.

Specific to JS is that the latter task must wait for the completion of the previous task will start to execute, hit a “difficult” (long time) task, will cause blocking. Of course, synchronization is a guarantee of program correctness for tasks that depend on the execution results of previous tasks.

2. The asynchronous

Continue with the year-end bonus example.

This year, the company is rich and has set up an OA system. All employees’ attendance and welfare are put on the system. The employees are still those employees, but this year’s year-end bonus is no longer in line, the boss said that the year-end bonus will be issued today, the accountant will start the system procedure, all the employees’ year-end bonus will be calculated by the system itself, until which employee’s year-end bonus is calculated, and then the accountant will directly transfer the money to the employee’s account.

Have all employees’ annual bonuses calculated “at the same time”, so employees can do what they like instead of waiting in line.

Back in JS, asynchrony allows multiple tasks to be executed at the same time, and each asynchronous task can hand over its own work to other threads. After the execution is completed, the execution result is given to the main thread to continue execution. It can avoid blocking, reduce task waiting time and improve user experience.

For the main thread, asynchrony is the equivalent of splitting the task into two phases, executing the first phase and then executing the second phase “when the time is right”.

2. Asynchronous scenario

There are several common asynchronous scenarios

  1. Respond to user actions: Respond to user input, such as a mouse click at a specific location
  2. Specify the time to execute: setTimeout, setInterval, requestAnimationFrame
  3. Large number of operations: complex operations with large data scale; But generally put the server, little front-end processing
  4. Request server data: Send asynchronous requests using XMLHttpRequest

In the first two scenarios, the browser itself implements asynchrony.

The amount of computation that would have been a headache in the past can now be done using a Web Worker, as shown in the example simple-web-worker.

When we think of async, we think of Ajax requests, XMLHttpRequest requests, so we’ll focus on communicating with the background.

Three, asynchronous request history

The asynchronous request solution goes through roughly four phases:

  1. The callback function
  2. Promise
  3. Generator
  4. async/await

Callbacks dominated the early frontal siege lions for a long time, until ES6 brought a bunch of new features that put an end to the slash-and-burn “callbacks.”

4. Callback function

Callbacks are JavaScript’s answer to asynchronous programming. As mentioned above, asynchron can be thought of as splitting a task into two parts, with the first part executed first and the second part executed after the asynchronous task has produced results. A callback, on the other hand, writes the second paragraph to a function and then calls the function directly when it needs to be executed.

Here is an example of a callback in Node:

function callback(err, data) {
  console.log(data);
}
fs.readFile("./test.txt"."utf8", callback);
Copy the code

The third argument to readFile is a callback function, which is the second section of the task. The callback function is executed when the operating system successfully reads test.txt or an error occurs. (Note: In node, the first argument to the callback must be an error object, because after the first paragraph is executed, the context has ended, and any subsequent error thrown cannot be caught.)

If a callback function also has asynchronous operations, then the callback function is nested, as shown in the following example:

fs.readFile("./a.txt"."utf8".function (err, data) {
  console.log("file a", data);
  fs.readFile("./b.txt"."utf8".function (err, data) {
    console.log("file b", data);
    fs.readFile("./c.txt"."utf8".function (err, data) {
      console.log("file c", data);
    });
  });
});
Copy the code

After the a. TB file is read or an error is reported, read the B. TB file. After the B. TB file is read successfully or an error is reported, read the C. TB file. The nesting of only three callbacks is shown here; there are many more in a real project.

Callback function nesting not only affects reading and later maintenance; At the same time, strong coupling is formed between multiple asynchronous operations. If one of the callbacks changes, both the one before and the one after the callback will have to be changed, which is called callback hell.

Five, the Promise

In ES2015 (ES6), Promise was introduced to solve the problem of callback hell by adopting the method of chained call.

Promise is a ThEnable object that can get messages for asynchronous operations with three states:

  • Pending: The initial state after the Promise instance is created. It also indicates that the task is being executed.
  • This is very depressing. The task has been fulfilled successfully. The state transformed after calling resolve in the executor;
  • Rejected: The mission has failed and has been rejected. The state transformed after reject is called in the executor.

The Promise constructor receives an executor function that takes two arguments (resolve and reject) and determines whether to use resolve or reject to change the state of the Promise instance based on the result of the asynchronous task in the executor. This is very depressing. A Promise in the pending state can only be fulfilled.

  • The resolve call changes the pending state to a fulfilled state
  • The reject call changes the Pending state to rejected

When the state changes, the handler for the Promise THEN method is called

  • When this state is fulfilled, the first handler (the first argument) bound to the THEN method is called
  • The second handler (second argument) of the then method binding is called when it changes to the Rejected state or when an error is reported; If the second handler is unbound, the handler of the catch method is called

The promise.prototype. then and promise.prototype. catch methods both return a new Promise object, so they can be called chained. There is also a Promise is not commonly used method. The prototype. Finally, the same is to return a new Promise object; Once the promise is fulfilled, the finally handler will be called, regardless of whether the promise is fulfilled or failed.

A static method

methods parameter Trigger a success Trigger failure
Promise.all A collection of Promise objects All promises were successful Failure of any promise triggers an immediate failure
Promise.allSettled A collection of Promise objects All promises are executed, regardless of status
Promise.any A collection of Promise objects If one of the promises succeeds, it fires, returning a successful promise All the promises have been rejectedAggregateError
Promise.race A collection of Promise objects This will be fulfilled fulfilled for the first promise The first promise that has been executed is in the rejected state
Promise.reject Refuse to reason Is called
Promise.resolve The value of success Is called

CustomPromise

Promise is often the front end of the interview to ask knowledge points, the following by realizing a simple Promise to in-depth understanding.

function CustomPromise(executor) {
  // value Records the successful execution result of the asynchronous task
  this.value = null;
  // reason Indicates the reason for asynchronous task failure
  this.reason = null;
  // status Records the current state. Initialization is pending
  this.status = "pending";
  // Cache two queues and maintain the resolved and Rejected handlers
  this.onResolvedQueue = [];
  this.onRejectedQueue = [];

  var self = this;

  // Define the resolve function
  function resolve(value) {
    // If it is pending, return it directly
    if(self.status ! = ="pending") {
      return;
    }
    // The asynchronous task succeeds, and the result is assigned to value
    self.value = value;
    // The current state is resolved
    self.status = "resolved";
    // Batch out the resolved queue
    self.onResolvedQueue.forEach((resolved) = > resolved(self.value));
  }

  // Define the reject function
  function reject(reason) {
    // If it is pending, return it directly
    if(self.status ! = ="pending") {
      return;
    }
    // The asynchronous task fails, and the result is assigned to value
    self.reason = reason;
    // The current state is changed to Rejected
    self.status = "rejected";
    // Delay execution of queued tasks with setTimeout
    // Execute the tasks in the rejected queue in batches
    self.onRejectedQueue.forEach((rejected) = > rejected(self.reason));
  }

  // Assign the resolve and reject capabilities to the executor
  executor(resolve, reject);
}
Copy the code

CustomPromise receives an executable that implements the promise three states and the Resolve Reject function, focusing on two queues: onResolvedQueue and onRejectedQueue.

// The then method takes two functions as parameters (optional)
CustomPromise.prototype.then = function (onResolved, onRejected) {
  if (typeofonResolved ! = ="function") {
    onResolved = function (x) {
      return x;
    };
  }
  if (typeofonRejected ! = ="function") {
    onRejected = function (e) {
      throw e;
    };
  }
  var self = this;
  let x;

  // New Promise object
  var promise2 = new CustomPromise(function (resolve, reject) {
    // Determine the state and assign the corresponding processing function
    if (self.status === "resolved") {
      // The resolve handler
      resolveByStatus(resolve, reject);
    } else if (self.status === "rejected") {
      // Reject handler
      rejectByStatus(resolve, reject);
    } else if (self.status === "pending") {
      // If pending, the task is pushed to the corresponding queue
      self.onResolvedQueue.push(function () {
        resolveByStatus(resolve, reject);
      });
      self.onRejectedQueue.push(function () { rejectByStatus(resolve, reject); }); }});// Resolve state handler
  function resolveByStatus(resolve, reject) {
    // Wrap it as an asynchronous task to ensure that the resolver executes after then
    setTimeout(function () {
      try {
        // The return value is assigned to x
        x = onResolved(self.value);
        // Enter the resolution process
        resolutionProcedure(promise2, x, resolve, reject);
      } catch (e) {
        // If onResolved or onRejected throws an error, promise2 must be rejectedreject(e); }}); }// Reject
  function rejectByStatus(resolve, reject) {
    // Wrap it as an asynchronous task to ensure that the resolver executes after then
    setTimeout(function () {
      try {
        // The return value is assigned to x
        x = onRejected(self.reason);
        // Enter the resolution process
        resolutionProcedure(promise2, x, resolve, reject);
      } catch(e) { reject(e); }}); }// Make promise2 return
  return promise2;
};
Copy the code

The then function accepts two handler functions, determines whether the two parameter types are functions, and returns or throws them. New A new CustomPromise object that determines the state of the calling object

  • If the state is Resolved, call resolveByStatus.
  • If the state is Rejected, then rejectByStatus is called;
  • If the state is still pending, then resolveByStatus is pushed to onResolvedQueue and rejectByStatus to onRejectedQueue. The main logic of resolveByStatus and rejectByStatus is the resolution procedure (PROMISE /A+ standard)
function resolutionProcedure(promise2, x, resolve, reject) {
  // hasCalled is used to ensure that resolve and reject are not repeated
  let hasCalled;
  if (x === promise2) {
    Reject if the result of resolve is the same as that of Promise2. This is to avoid an infinite loop
    return reject(new TypeError("In order to avoid an infinite loop, this is a mistake."));
  } else if(x ! = =null && (typeof x === "object" || typeof x === "function")) {
    // Spec: if x is an object or function, some additional processing is required
    try {
      // The first thing to do is to see if it has a then method.
      let then = x.then;
      // If it is a thenable object, then point the PROMISE's THEN method to x. hen.
      if (typeof then === "function") {
        then.call(
          x,
          (y) = > {
            // If the object has been resolved/rejected, return it directly
            if (hasCalled) return;
            hasCalled = true;
            // Enter the resolver (recursively calling itself)
            resolutionProcedure(promise2, y, resolve, reject);
          },
          (err) = > {
            // The use of hascalled is the same as above
            if (hasCalled) return;
            hasCalled = true; reject(err); }); }else {
        // If then is not function, execute the promise with x as the argumentresolve(x); }}catch (e) {
      if (hasCalled) return;
      hasCalled = true; reject(e); }}else {
    // If x is not an object or function, execute the promise with x as the argumentresolve(x); }}Copy the code

ResolutionProcedure takes four parameters

  • Promise2: A new promise object
  • X: the value returned by the resolve or reject function bound to the called object
  • Resolve: the resolve function bound to the PROMISE2 object
  • Reject: Reject function bound to a PROMISe2 object

Determine the type of x

  • Is equal to Promise2: throws an exception
  • The thenable object: points this to x, taking the resolve handler first and the reject handler second; Resolve handles the function call itself
  • Other types: Call resolve

The sample

Continue with the example above, this time with a Promise

function readFilePromise(filePath) {
  return new Promise((resolve, reject) = > {
    fs.readFile(filePath, "utf8".(err, data) = > {
      if (err) {
        return reject(err);
      }
      resolve(data);
    });
  });
}
/ / use
readFilePromise("./a.txt")
  .then((data) = > {
    console.log("Document:", data);
    return readFilePromise("./b.txt");
  })
  .then((data) = > {
    console.log("Document:", data);
    return readFilePromise("./c.txt");
  })
  .then((data) = > {
    console.log("Document:", data);
  })
  .catch((err) = > {
    console.error(err);
  });
Copy the code

Here, we wrap the readFile with a Promise, and we read all three files with a chain call to the THEN method, and catch any errors thrown during execution with the catch method at the end.

Compared to the callback method, the code structure is clearer and the execution sequence is clear. But otherwise it’s nothing new. It’s more like an improvement on the callback function, an accumulation of then methods, and the semantics are still unclear.

Is there a better way? And the answer is yes. Let’s move on

Generator functions

Generator functions (Generator functions) are also a new syntax introduced in ES6 to address asynchronous programming.

Declare a Generator function with function*. Instead of executing immediately, the called function returns an iterator object for the Generator. (Note: es6 does not specify where the asterisk is between function and function name)

The returned iterator object provides the following methods:

The method name parameter The return value describe
Generator.prototype.next() Value: The value passed to the generator {done: true if it has been executed to the end and returned, value: any JavaScript value returned} The pointer is executed down until either yield or return is encountered, returning a value generated by the yield expression
Generator.prototype.return() Value: the value to be returned Returns the value given as an argument to the function Returns the given value and terminates the generator
Generator.prototype.throw() Exception: The exception that is thrown {done: true if it has been executed to the end and returned, value: any JavaScript value returned} Throws an error to the generator

Generator functions are more like containers for asynchronous tasks because they “control” their execution, which can be interrupted or reawakened.

The execution of the Generator function will be suspended if:

  1. The yield yield keyword can only be used in Generator functions. The execution of the Generator function is interrupted by the yield keyword until the Generator’s next() method is called. The next() method also returns an IteratorResult object with two properties, value and done.

  2. The yield* yield* keyword is followed by a generator or iterable. The execution of a function that encounters yield* also suspends execution, delegating control to the object after yield*, and executing the generator or iterable following yield* by calling the next() method. Yiel * is an expression that returns the value directly to the caller.

  3. After the return function is executed, return does not return the value directly to the caller. It is still required to call next(). The value returned is specified by the return statement, and done is true.

  4. The throw throws an exception, and the function completely stops execution.

  5. The trailing function of the generator function is executed, and the next() method is called again with value equal to undefined and done true.

Here’s an example:

function* g1() {
  console.log("g1 start");
  yield* [1.2.3];
  return "foo";
}

var result, yieldResult;

function* g2() {
  console.log("g2 start");
  result = yield* g1();
  yieldResult = yield "g5 yield";
  return "g5 inner";
}

var iterator = g2();

console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}

console.log(iterator.next()); // {value: "g5 yield", done: false};}
console.log(iterator.next()); // {value: "g5 inner", done: true}

console.log(result); // foo
console.log(yieldResult); // undefined
Copy the code

We declare two generator functions g1 and g2, call g2, and assign the returned iterator object to the iterator.

  • The g2 function is not executed immediately, but can only be executed by calling the next() method, printing out g2 start first, and continuing execution meets the yield* expression.

  • Yield * delegates control to the function g1, which starts executing, printing out g1 start, and also yields *, which also delegates control to the iterable, which is the array [1, 2, 3] here, and returns the first element 1 as a value. {value: 1, done: false} is displayed.

  • Control is still in the array, so continuing to call next() still reads the elements in the array, and prints {value: 2, done: false}, {value: 3, done: false} in turn.

  • When all the elements in the array are read, control is returned to G2. When next() is called, g2 continues, assigning the value of foo returned by G1 to result, and output {value: “g5 yield”, done: False}, yield, unlike yield*, has no value of its own, so yieldResult is undefined.

  • Further down the line, the return is encountered, and the execution is interrupted until the next next() method is called, with {value: “g5 inner”, done: true}.

The sample

Finally, refine the above example with the Generator function

function readFilePromise(filePath) {
  return new Promise((resolve, reject) = > {
    fs.readFile(filePath, "utf8".(err, data) = > {
      if (err) {
        return reject(err);
      }
      resolve(data);
    });
  });
}
function* readFileGen() {
  yield readFilePromise("./a.txt");
  yield readFilePromise("./b.txt");
  yield readFilePromise("./c.txt");
}

function run() {
  const gen = readFileGen();
  function next() {
    const { done, value } = gen.next();
    if (done) {
      // Go to the end
      console.log("done", value);
      return value;
    }
    value.then((data) = > {
      console.log("File", data);
      next();
    });
  }
  next();
}

run();
Copy the code

Here the readFilePromise function is rewrapped with a Generator function to read three files in turn, requiring a call to the next function to execute. Run is a self-executing function, and gen is the object returned by the Generator function. We control the execution of the pointer by calling gen.next(). The returned object contains the done and value properties. If done is true, return value. If done is true, return value. If done is not true, then the next method is called recursively. In this case, value is a Thenable object, so print out the data in the then handler and execute the next method.

Compare that to promise’s chained calls, which have more calls to the next method instead of the then stack. It’s more powerful, but it feels more complicated.

Seven, Async/Await

Async and await are introduced in the ES2017 standard to handle asynchronous tasks.

Async function

Async functions are functions declared using the async keyword. Async functions always return a promise object. In other words, async functions are the syntax sugar of Generator functions.

What does this grammar sugar do? It can be roughly divided into the following points:

  1. Built-in actuator

Generator functions must be executed by the actuator, which is the run function in the example in the previous section. Async functions have their own actuators. Async functions can be called directly, just like ordinary functions. The syntax of async and await functions is more semantically clear than the asterisk and yield of Generator functions. Async indicates that there are asynchronous operations in the function, and await indicates that the following expression needs to wait for the result. The Async function always returns a Promise object, which is much more convenient than the IteratorResult object returned by Generator functions. Async can be thought of as wrapping multiple operations within a function into a Promise.

await

Await keyword is generally used with async and can only be used with async functions. Await is usually followed by a Promise object, and if it is not a Promise object it is converted to an immediate resolve Promise object.

As usual, let’s do a simple little example

function promiseFunc() {
  return new Promise((rs, rj) = > {
    console.log("promise");
    rs("hello world");
  });
}
async function asyncFunc() {
  console.log("async start");
  const result = await promiseFunc();
  console.log(result);
}
asyncFunc();
console.log("main thread");
Copy the code

The output

// async start
// promise
// main thread
// hello world
Copy the code

Why is main thread printed before Hello World?

  • The asyncFunc function is interrupted while the promiseFunc is waiting for the “await” keyword to complete. The asyncFunc function is interrupted and the control is returned to the caller. Continue with console.log(‘main thread’) and output main thread.
  • PromiseFunc () asyncFunc ()

This is where microtasks/macros come in. See my previous article EventLoop and Microtask Macros from browser Principles.

In this example, the async function can be seen as wrapping multiple asynchronous tasks (Promises) into a single promise, and the await is like the syntactic sugar of the asynchronous operation then command.

Error handling

Async /await syntax is good, but error handling is a problem. In this example, if promiseFunc uses rj(‘hello world’) instead of RS (‘hello world’), it will output “Hello world” as well.

So methods usually throw a value when they need to call Reject, and async functions are wrapped in try… Catch in order to handle failures and rejections.

The sample

Finally, the final version of readFile

function readFilePromise(filePath) {
  return new Promise((resolve, reject) = > {
    fs.readFile(filePath, "utf8".(err, data) = > {
      if (err) {
        return reject(err);
      }
      resolve(data);
    });
  });
}
async function run() {
  const a = await readFilePromise("./a.txt");
  console.log("File", a);
  const b = await readFilePromise("./b.txt");
  console.log("File", b);
  const c = await readFilePromise("./c.txt");
  console.log("File", c);
}
run();
Copy the code

Using async/await writing to handle asynchronous tasks looks more semantically appropriate and the code is cleaner.

The above.

Afterword.

If you have any other comments, please feel free to discuss them in the comments section. The article is also posted on the personal public account, welcome to follow MelonField

reference

  • Promise [OL] developer.mozilla.org/zh-CN/docs/…
  • The Generator [OL] developer.mozilla.org/zh-CN/docs/…
  • Async function [OL] developer.mozilla.org/zh-CN/docs/…
  • Await [OL] developer.mozilla.org/zh-CN/docs/…
  • Yield [OL] developer.mozilla.org/zh-CN/docs/…
  • Yield * [OL] developer.mozilla.org/zh-CN/docs/… *
  • Ruan Yifeng. Introduction to ES6 Standard [M]. 3rd edition