Make writing a habit together! This is the second day of my participation in the “Gold Digging Day New Plan · April More text challenge”. Click here for more details.

Introduction to the

As mentioned earlier, JS is divided into synchronous tasks and asynchronous tasks. But what are the asynchronous solutions for asynchronous tasks? Today, I will talk about the common asynchronous solutions in JS.

Asynchronous solution

There are five common asynchronous solutions in JS

  1. Callback function
  2. Event listeners
  3. Promise
  4. Generator
  5. Async/Await

Callback function

A callback function is simply a function passed as an argument to another function. Callbacks are the simplest solution to asynchrony.

For example, if we want to asynchronously output the result we got from the back end after the request, we can use the callback function.

function fn1(callback){
  // Simulate an Ajax request to fetch back-end data, taking one second
  setTimeout(() = > {
    const data = 'I'm the result of the back end.'
    callback && callback(data)
  }, 1000)
}

fn1((data) = >{
  console.log(data)
})
Copy the code

In the example above (data)=>{console.log(data)} is the callback function, which is passed as an argument to another function. The callback function is executed when the asynchronous task has a result, without blocking the execution of the main program. Therefore, the callback function is the first asynchronous solution used in JS.

The disadvantages of using callback functions are also obvious. Passing functions as arguments makes it difficult to read and maintain code, and the parts are highly coupled. And it’s easy to create callback hell if multiple callback functions are nested.

Such as

fun1(() = > {
  fun2(() = > {
    fun3()
  })
})
Copy the code

Event listeners

Event monitoring is introduced in js in the previous article. If you are not familiar with it, you can look at it again. We can implement asynchrony using custom events and listening.

We’ve already shown that there are two ways to create custom events, so we can use different methods in different situations.

Create custom events using new Event(), which does not pass parameters and is suitable for simple scenarios.

const myEvent = new Event("test");

// Listen on events
document.addEventListener("test".function (e) {
  // Similar to the callback function, can be handled here
  console.log("Custom event triggered");
});

// Triggers a custom event
setTimeout(function () {
  if (document.dispatchEvent) {
    document.dispatchEvent(myEvent);
  } else {
    // Compatible with older browsers
    document.fireEvent(myEvent); }},2000);
Copy the code

Create a CustomEvent using new CustomEvent(), which can pass parameters and is suitable for scenarios where parameters need to be passed.

// Create a custom event that can pass parameters. You must use detail as the key otherwise you cannot get it
const myEvent2 = new CustomEvent("test2", { detail: { name: "randy"}});// Listen on events
document.addEventListener("test2".function (e) {
  // Similar to the callback function, can be handled here
  console.log("Custom event triggered parameter is", e.detail);
});

// Triggers a custom event
setTimeout(function () {
  if (document.dispatchEvent) {
    document.dispatchEvent(myEvent2);
  } else {
    // Compatible with older browsers
    document.fireEvent(myEvent2); }},3000);
Copy the code

Using event listeners is a solution to asynchrony, but the entire program has become event-driven, and each time you use it, you have to register event listeners and then trigger them.

Promise

The Promise object was introduced in ES2015 (ES6) as a solution to asynchronous programming.

Characteristics of Promise

The Promise object represents an asynchronous operation that has three states

  • In the (Pending)
  • Has been completed (Resolved/Fulfilled)
  • Has failed (Rejected)

Only the result of an asynchronous operation can determine the current state, and no other operation can change the state.

Once the state of the Promise object changes, it will not change again. There are only two possibilities for the state of the Promise object to change

  • Changed from Pending to Resolved
  • Change from Pending to Rejected

As long as those two things happen, the state is frozen, it’s not going to change, it’s going to stay the same.

Promise the related API

new Promise()

Promise is a constructor, and we can either create a Promise instance with the new keyword or use some of the static methods of Promise directly.

new Promise((resolve, reject) = >{... });Copy the code

The processor function accepts two arguments, resolve and reject, which are also two callback functions

The resolve function is called when the asynchronous operation succeeds and passes the result of the asynchronous operation as an argument

The reject function is called when an asynchronous operation fails and passes the error reported by the asynchronous operation as an argument

The simple understanding is that one is a success callback and one is a failure callback.

For example, in the following example, we return success if the number is less than 5, failure otherwise.

function fun1() {
  return new Promise((resolve, reject) = > {
    const num = Math.ceil(Math.random()*10)
    if(num < 5){
      resolve(num)
    }else{
      reject('Numbers are too large')}}}Copy the code

We can also use some of the static methods of promises to create Promise objects

function fun2() {
  const num = Math.ceil(Math.random()*10)
  if(num < 5) {return Promise.resolve(num)
  }else{
    return Promise.reject('Numbers are too large')}}Copy the code

Promise.prototype.then()

After the Promise instance is generated, the then method can be used to specify the Resolved and Reject state callbacks.

Promise.prototype.then(onFulfilled[, onRejected])
Copy the code

The first argument is the Resolved callback, and the second is the Reject callback (normally we don’t use catch instead).

fun1().then(
  (res) = > {
    // res is the fun1 method resolve argument, which is a random number less than 5
    console.log(res);
  },
  (err) = > {
    // res is the fun1 reject argument, which means the number is too large
    console.log(err); });Copy the code

The catch method is used instead of the second argument to the then method, so we can rewrite the above example as

fun1()
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err);
  });
Copy the code

Why not use the second argument to the then method instead of the catch method? We know that promises are called in a chain, and in practice there will be a lot of THEN methods called, so if we put all the error handling in the THEN method, we will need to handle the error in each THEN. With the catch method, we simply add a catch method to the end of the chain call to catch the first error. This will greatly simplify our code.

thenMethod must return onepromiseobject

  1. ifthenMethod returns a common value (Number, String, etc.) and wraps a new one with that valuePromiseObject returns.
function fun3() {
  return Promise.resolve(1);
}

fun3()
  .then((res) = > {
    console.log(res); / / 1
    return "success"; // Returns a common value (Number, String, etc.)
  })
  .then((res) = > {
    console.log(res); // success
  });
Copy the code
  1. ifthenMethod returns onePromiseObject, then take that object and return its result.
function fun4() {
  return Promise.resolve(1);
}

fun4()
  .then((res) = > {
    console.log(res); / / 1
    return Promise.resolve("success2");
  })
  .then((res) = > {
    console.log(res); // success2
  });
Copy the code
  1. ifthenThere’s nothing in the methodreturnStatement, return a useundefinedThe packing ofPromiseObject.
function fun5() {
  return Promise.resolve(1);
}

fun5()
  .then((res) = > {
    console.log(res); / / 1
  })
  .then((res) = > {
    console.log(res); // undefined
  });
Copy the code
  1. ifthenIf an exception occurs in a method, it is passed to the nextthentheonRejectedOr the last onecatchMethods.
function fun6() {
  return Promise.resolve("fun6");
}

fun6()
  .then(
    (res) = > {
      console.log(Then1 Resolve method + res);
      return Promise.reject("error1");
    },
    (err) = > {
      console.log(Then1 Reject Method + err);
    }
  )
  .then(
    (res) = > {
      console.log(Then2 Resolve method + res);
    },
    (err) = > {
      console.log("Then2 Reject Method"+ err); });Copy the code

The above example prints then1 resolve fun6 and then2 reject error1

function fun6() {
  return Promise.resolve("fun6");
}

fun6()
  .then((res) = > {
    console.log(Then1 Resolve method + res);
    return Promise.reject("error1");
  })
  .then((res) = > {
    console.log(Then2 Resolve method + res);
    return Promise.reject("error2");
  })
  .catch((err) = > {
    console.log("Catch" + err);
  });
Copy the code

The above example prints then1 resolve method fun6 and catch method error1

  1. ifthenMethod passes no callbacks and continues down (so-called value penetration)
function fun7() {
  return Promise.resolve("fun7");
}

fun7()
  .then()
  .then()
  .then((res) = > {
    console.log(res); // fun7
  });
Copy the code

Promise.prototype.catch()

The catch method, also described earlier, is mainly used to catch errors. This method also returns a new Promise object.

If there is an error in the catch method, the Promise returned via catch is an instance of Rejected, otherwise it is a successful Resolved instance.

function fun8() {
  return Promise.reject("fun8");
}

fun8()
  .catch((err) = > {
    console.log(err); // fun8
    // The resolve callback of the next THEN method is entered if the throw is correct, and the reject callback is entered otherwise
    // throw new Error("11"); 
  })
  .then(
    (res) = > {
      console.log("resolve" + res);
    },
    (err) = > {
      console.log("reject"+ err); });Copy the code

Rather than using the second argument of then, I recommend using the catch method to catch errors.

Promise.prototype.finally()

Finally, in English, means the last, and this method is new to ES2018.

Finally, at the end of the Promise, success or failure is executed and the callback method has no arguments.

function fun9() {
  return Promise.reject("fun9");
}

fun9()
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err);
  })
  .finally(() = > {
    console.log("I'll do it resolve or reject.");
  });
Copy the code

This method is suitable for writing both then() and catch() at the same time. For example, we need to disable the loading state regardless of whether the request succeeds or not. We can disable the loading state by setting loading=false in the finally callback method.

Promise.resolve()

This method is static and, as we briefly explained earlier in the new Promise() constructor, takes an argument and converts it to a Promise object.

Promise.resolve(value)

/ / similar to
new Promise((resolve, reject) = > {
  resolve(value)
})
Copy the code

Promise.reject()

This method is static and, as we briefly explained earlier in the new Promise() constructor, takes an argument and converts it to a Promise object.

Promise.reject(value)

/ / similar to
new Promise((resolve, reject) = > {
  reject(value)
})
Copy the code

Promise.all()

The promise.all (iterable) argument is an array of Promise instances used to wrap multiple Promise instances into a new Promise instance.

Promise. All () is Resolved when all Promise instances in the array are Resolved, otherwise it is Rejected. And Rejected is the return value of the first Rejected Promise.

function fun10() {
  return Promise.resolve("fun10");
}
function fun11() {
  return Promise.resolve("fun11");
}
function fun12() {
  return Promise.resolve("fun12");
}
Promise.all([fun10(), fun11(), fun12()])
  .then((res) = > {
    console.log(res); // ['fun10', 'fun11', 'fun12']
  })
  .catch((err) = > {
    console.log(err);
  });
Copy the code
function fun10() {
  return Promise.resolve("fun10");
}
function fun11() {
  return Promise.reject("fun11");
}
function fun12() {
  return Promise.reject("fun12");
}
Promise.all([fun10(), fun11(), fun12()])
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err); // output fun11, the first value returned by 'Rejected' and 'Promise'
  });
Copy the code

Promise.race()

The RACE method differs from the ALL method in that whenever an object’s state changes, its state changes and the return value of that instance is passed to the callback function.

The return value of the first Promise is the return value of the race method regardless of failure or success. Resolve goes to then, otherwise it goes to catch

function fun13() {
  return new Promise(function (resolve, reject) {
    setTimeout(() = > {
      return resolve("fun13");
    }, 2000);
  });
}
function fun14() {
  return new Promise(function (resolve, reject) {
    setTimeout(() = > {
      return resolve("fun14");
    }, 1000);
  });
}

Promise.race([fun13(), fun14()])
  .then((res) = > {
    console.log("race resolve" + res); // race resolvefun14
  })
  .catch((err) = > {
    console.log("race error" + err);
  });
Copy the code

An incorrect catch thrown in an asynchronous callback cannot be caught

First, let’s look at situations where errors can be caught by throwing them directly in the processor function of the Promise object.

function fun15() {
  return new Promise(function (resolve, reject) {
    throw new Error("This is a mistake.");
  });
}

fun15()
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err); // Error: This is an Error
  });
Copy the code

In the following code, emulate an asynchronous throw in the handler function of the Promise object, in which case no errors are caught and an Uncaught Error is reported. The reason? Here involves the micro task and macro task, not clear can see the author js execution context and execution mechanism of the article said through this article.

function fun16() {
  return new Promise(function (resolve, reject) {
    setTimeout(() = > {
      throw new Error("This is a mistake.");
    }, 1000);
  });
}

fun16()
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err); // Uncaught Error: This is an Error
  });
Copy the code

So what’s the solution? This requires a try catch, then a reject after the catch.

function fun17() {
  return new Promise(function (resolve, reject) {
    setTimeout(() = > {
      try {
        throw new Error("This is a mistake.");
      } catch(err) { reject(err); }},1000);
  });
}

fun17()
  .then((res) = > {
    console.log(res);
  })
  .catch((err) = > {
    console.log(err); // Error: This is an Error
  });
Copy the code

Promise writes asynchronous code synchronously, avoids layers of nested callbacks, and provides apis that make it easier to use. But Promise’s chaining calls always concatenate code, too.

Generator

Generator was also introduced in ES2015 (ES6) as a solution to asynchronous programming, with the greatest feature being the ability to hand over execution of functions.

Generator functions need to be used in conjunction with the yield keyword.

grammar

* indicates that functions are Generator functions and can only be used in function functions, not arrow functions.

The yield field is used to define the state inside the function and to yield execution rights. This keyword can only occur in generator functions, but generators may not have the yield keyword. The function will pause at yield and throw out the result of the expression following the yield.

You can call a Generator just as you would a normal function. The Generator does not execute immediately as a normal function. Instead, it returns a pointer to an internal state object, similar to the next method of the Iterator object. We need to manually call the next method for the next operation, and the pointer will be executed either from the head of the function or from where it was last stopped.

Iterators may be involved here, but if you don’t know, you can read the section on Iterator that I wrote earlier

Function *fn(); function*fn(); function*fn()
function* generatorFn() {
  console.log("a");
  yield "1";
  console.log("b");
  yield "2";
  console.log("c");
  return "3";
}

const generatorIt = generatorFn();
console.log(generatorIt.next()); // {value: '1', done: false}
console.log(generatorIt.next()); // {value: '2', done: false}
console.log(generatorIt.next()); // {value: '3', done: true}
Copy the code

Analyze the execution of Generator functions

{value: ‘1’, done: false}, b, {value: ‘2’, done: false}, c, {value: ‘3’, done: True} Let’s analyze the execution of the Generator function

  1. First the Generator function executes, returning a pointer to the internal state object, generatorIt, with no output.

  2. The first call to the next method, starting at the head of the Generator function, prints a, stops at yield, and executes the expression immediately following yield, taking the expression’s value ‘1’ as the value of the return object’s value property, while the function is still executing. The done property value of the returned object is false.

  3. The next method is called a second time, starting at the first yield, printing b, stopping at the second yield, and executing the expression immediately following the yield, taking the expression’s value ‘2’ as the value of the object’s value property. The done property value of the returned object is false.

  4. The next method is called a third time, running from the second yield, printing C, and then performing the function’s return operation, using the value of the expression following the return as the value of the returned object’s value property. At this point, the function is finished, so the done property value is true. {value: undefined, done: true} is returned if the function does not return a value at this step.

Is there any general understanding of Generator functions at this point? The simple understanding is that the Generator function yield stops where it is placed (note that the statement immediately following yield is executed and returns the value as the iterator) and is called step by step using the next method.

Analyze the execution of the Generator function again

Are statements following yield executed next or next? Many small partners are not clear, so the author gives an example to analyze.

function* generatorFn2() {
  console.log("a");
  yield console.log("a2");
  console.log("b");
  yield console.log("b2");
  console.log("c");
}

const generatorIt2 = generatorFn2();
console.log(generatorIt2.next()); // {value: undefined, done: false}
console.log(generatorIt2.next()); // {value: undefined, done: false}
console.log(generatorIt2.next()); // {value: undefined, done: true}
Copy the code

{value: undefined, done: false}, b, b2, {value: undefined, done: false}, c, {value: undefined, done: false} : True}, so we can see from this example that the statement immediately following yield is executed at the time.

Const res = yield “1”; {value: ‘1’, done: false}? Now let’s analyze it again

for.. Of traversal of the Generator

As stated in the section on Iterator, for of can be used whenever an Iterator is implemented, so we can use for of to run our Generator functions, so we don’t need to call next manually. Once the done property of the object returned by the next method is true, for… The of loop terminates and does not contain the return object. How do I understand that and do not include the return object? That is, when done is true it terminates the loop so the last item is never printed out.

function* generatorFn() {
  console.log("a");
  yield "1";
  console.log("b");
  yield "2";
  console.log("c");
  return "3";
}

for (const iterator of generatorFn()) {
  console.log(iterator); // a, 1, b, 2, c
}
Copy the code

The above example prints a, 1, b, 2, and c, but not 3, so the loop terminates when done is true, so the last item is not printed.

So the Generator functions can be executed automatically by using the for of method without manually calling the next method, but the last item is not printed, so be careful.

The Generator function passes parameters

function* generatorFn3() {
  console.log("a");
  const res = yield "1";
  console.log(res); // undefined
}

const generatorIt3 = generatorFn3();
generatorIt3.next()
generatorIt3.next()
Copy the code

{value: ‘1’, done: false} instead of a, {value: ‘1’, done: false} So how do we send the reference? That’s where the next method comes in. So let’s rewrite the above example.

function* generatorFn3() {
  console.log("a");
  const res = yield "1";
  console.log(res); // randy
}

const generatorIt3 = generatorFn3();
generatorIt3.next()
generatorIt3.next('randy') // Pass the parameter through the next method
Copy the code

The code above prints a, then Randy. So the argument can be passed by next, which means that when it is passed by next, the argument will be returned as yield of the previous step.

Yield expression *

The yield command is followed by an asterisk to indicate that it returns a traverser, which is called the yield* expression.

function *foo(){
  yield "foo1"
  yield "foo2"
}
function *bar(){
  yield "bar1"
  yield* foo()
  yield "bar2"
}
for(let val of bar()){
  console.log(val)
}

// bar1 foo1 foo2 bar2
Copy the code

If the yield command is not followed by an asterisk, the entire array is returned, and the asterisk indicates that the array traverser is returned

function* gen1(){
  yield ["a"."b"."c"]}for(let val of gen1()){
  console.log(a)
}
// ["a", "b", "c"]

function* gen2(){
  yield* ["a"."b"."c"]}for(let val of gen2()){
  console.log(a)
}
// a b c
Copy the code

The return of the Generator

The return method returns the given value and completes traversing the Generator, returning undefined if the return does not pass a value.

function* foo() {
  yield 1;
  yield 2;
  yield 3;
}

var f = foo();
console.log(f.next());
{value: 1, done: false}

console.log(f.return("hahaha"));
{value: "hahaha", done: true}

console.log(f.next());
{value: undefined, done: true}
Copy the code

Generator functions provide elegant flow control that allows function execution to be interrupted at a given location, but their execution is dependent on the executor and is not very useful for simple asynchronous problems.

Async/Await

Async functions have been introduced in ES2017 to make asynchronous operations more convenient. The emergence of Async/Await is considered by many to be the ultimate and most elegant solution to ASYNCHRONOUS JS operations. Async/Await = Generator + Promise

grammar

Async is used to declare that a function is asynchronous, and await is used to wait for an asynchronous method to complete, and only then will the execution continue. Await is not required and await can only occur in async functions.

async function() {
  const result = await getData()
  console.log(result)
}
Copy the code

Isn’t it comfortable to write synchronous code to handle asynchrony?

Error handling

Async/Await doesn’t have as many apis as Promise, and errors need to be handled using try catch yourself.

async function() {
  try{
    const result = await getData()
    console.log(result)
  } catch(e) {
    console.log(e)
  }
}
Copy the code

Async/Await is a very elegant way of writing synchronous code to deal with asynchronous problems, but Await blocks code, maybe later asynchronous code does not depend on the former, but still needs to wait for the former to complete, causing the code to lose concurrency.

The author uses Async/Await and Promise as an example.

function getData() {
  return Promise.resolve("Analog acquisition of back-end data");
}

async function fun1() {
  console.log("Main program begins execution.");
  const result = await getData();
  console.log(result);
  console.log("Let the asynchronous code execute itself without blocking our main program.");
}

fun1(); // The main program starts executing, emulates the fetch of back-end data, and lets the asynchronous code execute without blocking the main program

async function fun2() {
  console.log("Main program begins execution.");
  getData().then((result) = > {
    console.log(result);
  });
  console.log("Let the asynchronous code execute itself without blocking our main program.");
}

fun2(); // The main program starts executing, leaving the asynchronous code to execute without blocking the main program, simulating the acquisition of back-end data
Copy the code

From the above example we can see the disadvantage of using Async/Await, which is that Async/Await will definitely block the execution of the following code regardless of whether the Async/Await result is depended on.

conclusion

In this paper, the author introduces five kinds of ASYNCHRONOUS JS solutions, each of which has its own characteristics. In different cases, we can choose the most appropriate solution to use. In general, I think Async/Await and Promise are currently the best asynchronous solutions.

extension

async defer

The async defer keyword is used to modify the

<script src='xxx'></script>
<script src='xxx' async></script>
<script src='xxx' defer></script>
Copy the code

When the browser is parsing HTML, if it encounters a Script tag without any attributes, it will pause parsing. It will first send a network request to obtain the code content of the JS script, and then ask the JS engine to execute the code. When the code execution is complete, it will resume parsing. The whole process is shown below:

When the browser encounters a script with async property, the network request for the script is asynchronous and will not block the browser from parsing HTML. Once the network request comes back, if the HTML has not been parsed, the browser will pause parsing, let the JS engine execute the code first, and then parse the code. The illustration is as follows:

When the browser encounters a script with the defer attribute, the web request to get the script is also asynchronous and does not block the browser from parsing the HTML. Once the web request comes back, if the HTML has not been parsed, the browser will not pause parsing and execute the JS code. Instead, wait for the HTML to be parsed before executing the js code, as shown below:

Based on the above analysis, the order in which different types of script are executed and whether they block parsing HTML is summarized as follows:

The script tag JS execution order Whether to block parsing HTML
<script> Order in HTML blocking
<script async> Network request return order It may or may not block
<script defer> Order in HTML Don’t block

Afterword.

Thank you for your patience, this article is the author’s personal learning notes, if there are fallacies, please inform, thank you! If this article is of any help to you, please click on the “like” button. Your support is my motivation to keep updating!