Explain in detail the four asynchronous solutions of JS: callback function, Promise, Generator, async/await (full of dry goods)

Get into the habit of writing together! This is the first day of my participation in the "Nuggets Daily New Plan · April Update Challenge"

Synchronous & Asynchronous Concepts

Before talking about these four asynchronous schemes, let's clarify the concepts of synchronization and asynchrony:

The so-called synchronization (synchronization) , in simple terms, is sequential execution , which means that only one thing can be done at the same time, and the next thing can only be done after the currently executing thing is completed. For example, if we go to the train station to buy tickets, if there is only one window, then only one person can purchase tickets at the same time, and the rest need to queue. This one by one action is synchronization. The advantage of synchronous operation is that everything is executed in sequence and orderly, and there is no problem of everyone grabbing a resource at the same time. The disadvantage of synchronous operations is that they block the execution of subsequent code . If the currently executing task takes a long time, the subsequent programs can only wait. This affects the efficiency. Corresponding to the display of the front-end page, it may cause the blocking of page rendering and greatly affect the user experience.

The so-called asynchronous (Asynchronization) , refers to the execution of the current code does not affect the execution of the following code. When the program runs to the asynchronous code, the asynchronous code is put into the task queue as a task instead of being pushed into the call stack of the main thread. After the main thread is executed, go to the task queue to execute the corresponding task. Therefore, the advantage of asynchronous operation is that it does not block the execution of subsequent code .

Asynchronous application scenarios in js

At the beginning, I talked about the concepts of synchronization and asynchrony, so what are the application scenarios of asynchronous in JS?

  • Scheduled tasks: setTimeout, setInterval
  • Network request: ajax request, dynamically created img tag loading
  • Event listener: addEventListener

Four ways to implement asynchrony

For asynchronous scenarios such as setTimeout, setInterval, and addEventListener, we do not need to manually implement asynchronous, just call them directly. But for ajax requests, the asynchronous operation of the database in node.js, we need to implement it ourselves~

1. Callback function

Before the emergence of microtask queues, the main way for JS to achieve asynchrony was through callback functions. Take a simple Ajax request as an example, the code structure is as follows:

function ajax(obj){
	let default = {
	  url: '...',
	  type:'GET',
	  async:true,
	  contentType: 'application/json',
	  success:function(){}
    };

	for (let key in obj) {
        defaultParam[key] = obj[key];
    }

    let xhr;
    if (window.XMLHttpRequest) {
        xhr = new XMLHttpRequest();
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }
    
    xhr.open(defaultParam.type, defaultParam.url+'?'+dataStr, defaultParam.async);
    xhr.send();
    xhr.onreadystatechange = function (){
        if (xhr.readyState === 4){
            if(xhr.status === 200){
                let result = JSON.parse(xhr.responseText);
                // 在此处调用回调函数
                defaultParam.success(result);
            }
        }
    }
}

复制代码

We can call the ajax request like this in the business code:

ajax({
   url:'#',
   type:GET,
   success:function(e){
    // 回调函数里就是对请求结果的处理
   }
});
复制代码

The success method of ajax is a callback function, and the callback function executes the further operations to be performed after our request is successful. In this way, asynchrony is initially realized, but the callback function has a very serious disadvantage, that is , the problem of callback hell . You can imagine, what if we initiate another ajax request in the callback function? Wouldn't it be necessary to continue writing an ajax request in the success function? What if multiple levels of nesting are required to initiate ajax requests? Wouldn't it require multiple levels of nesting? If the nesting level is deep, our code structure may become like this: 回调地狱示意图Therefore, in order to solve the problem of callback hell, the concepts of Promise, async/await, and generator are proposed.

2、Promise

As one of the typical micro-tasks, Promise can make JS achieve the effect of asynchronous execution. The structure of a Promise function is as follows:

const promise = new Promise((resolve, reject) => {
	resolve('a');
});
promise
    .then((arg) => { console.log(`执行resolve,参数是${arg}`) })
    .catch((arg) => { console.log(`执行reject,参数是${arg}`) })
    .finally(() => { console.log('结束promise') });
复制代码

If we need to execute asynchronous code nested, compared to callback functions, Promises are executed as shown in the following code:

const promise = new Promise((resolve, reject) => {
	resolve(1);
});
promise.then((value) => {
    	console.log(value);
    	return value * 2;
    }).then((value) => {
    	console.log(value);
    	return value * 2;
    }).then((value) => {
	  	console.log(value);
    }).catch((err) => {
		console.log(err);
    });
复制代码

That is, to achieve multi-level nesting ( chain calls ) through then, does this seem to be more comfortable than callback functions~

The lifecycle that every Promise goes through is:

  • In progress (pending) - code execution has not ended at this point, so it is also called unsettled (unsettled)
  • Settled - Async code has finished executing The processed code enters one of two states:
    • 已完成(fulfilled) - 表明异步代码执行成功,由resolve()触发
    • 已拒绝(rejected)- 遇到错误,异步代码执行失败 ,由reject()触发

因此,pending,fulfilled, rejected就是Promise中的三种状态啦~ 大家一定要牢记,在Promise中,要么包含resolve()来表示Promise的状态为fulfilled,要么包含reject()来表示Promise的状态为rejected。 不然我们的Promise就会一直处于pending的状态,直至程序崩溃...

除此之外,Promise不仅很好的解决了链式调用的问题,它还有很多神奇的操作呢:

  • Promise.all(promises):接收一个包含多个Promise对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略
  • Promise.allSettled(promises): 接收一个包含多个Promise对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为{status:'fulfilled' // 或 'rejected', value // 或reason}
  • Promise.race(promises): 接收一个包含多个Promise对象的数组,等待第一个有结果(完成/拒绝)的Promise,并把其result/error作为结果返回
function getPromises(){
    return [
        new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),
        new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),
        new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),
    ];
}

Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);
复制代码

打印结果如下:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

3、Generator

Generator是ES6提出的一种异步编程的方案。因为手动创建一个iterator十分麻烦,因此ES6推出了generator,用于更方便的创建iterator。也就是说,Generator就是一个返回值为iterator对象的函数。 在讲Generator之前,我们先来看看iterator是什么: iterator是什么? iterator中文名叫迭代器。它为js中各种不同的数据结构(Object、Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。 因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

iterator 的作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费

iterator的结构: 它有next方法,该方法返回一个包含valuedone两个属性的对象(我们假设叫result)。value是迭代的值,后者是表明迭代是否完成的标志。true表示迭代完成,false表示没有。iterator内部有指向迭代位置的指针,每次调用next,自动移动指针并返回相应的result。 使用iterator遍历对象的写法是:

function createIterator(items) {
  var i = 0;
  return {
    next: function () {
      var done = (i >= items.length);
      var value = !done ? items[i++] : undefined; 
      return {
        done: done,
        value: value
      };
    }
  };
}

var iterator = createIterator([1, 2, 3]);
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: undefined, done: true }"
// 后续的所有调用返回的结果都一样
console.log(iterator.next());  // "{ value: undefined, done: true }"
复制代码

使用iterator迭代器遍历数组的过程为:

let arr = ['a','b','c'];

let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
复制代码

for ... of的循环内部实现机制其实就是iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next()方法的任何对象,然后,在 for ... of 的每次循环中,都将调用该迭代器对象上的 .next 方法。然后使用for i of打印出来的i也就是调用.next方法后得到的对象上的value属性。

接下来,我们来聊聊Generator:
我们通过一个例子来看看Gnerator的特征:

function* createIterator() {
  yield 1;
  yield 2;
  yield 3;
}
// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
复制代码

Generator 函数是 ES6 提供的一种异步编程解决方案。形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星号
  • 函数体内部使用yield语句,定义不同的内部状态

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object

打印看看Generator函数返回值的内容: 在这里插入图片描述 发现generator函数的返回值的原型链上确实有iterator对象该有的next,这充分说明了generator的返回值是一个iterator。除此之外还有函数该有的return方法和throw方法。

在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。 但在generator中,就打破了这几点,所以generator和普通的函数完全不同。 当以function*的方式声明了一个Generator生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的Generator,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。

function* generator() {
    yield 1
    yield 2
};
let iterator = generator();
iterator.next()  // {value: 1, done: false}
iterator.next()  // {value: 2, done: false}
iterator.next()  // {value: undefined, done: true}
复制代码

yield和return的区别:

  • 都能返回紧跟在语句后面的那个表达式的值
  • yield相比于return来说,更像是一个断点。遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。
  • 一个函数里面,只能执行一个return语句,但是可以执行多次yield表达式。
  • 正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield

语法注意点:

  • yield表达式只能用在 Generator 函数里面

  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面

  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

  • 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效

使用Generator的其余注意事项:

  • 需要注意的是,yield 不能跨函数。并且yield需要和*配套使用,别处使用无效
function* createIterator(items) {
  items.forEach(function (item) {
    // 语法错误
    yield item + 1;
  });
}
复制代码
  • 箭头函数不能用做 generator

讲了这么多,那么Generator到底有什么用呢?

  • 因为Generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。
  • Generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个在ajax请求中很有用,避免了回调地狱.

4、 async/await

最后我们来讲讲async/await,终于讲到这儿了!!! async/await是ES7提出的关于异步的终极解决方案。我看网上关于async/await是谁的语法糖这块有两个版本:

  • 第一个版本说async/await是Generator的语法糖
  • 第二个版本说async/await是Promise的语法糖

其实,这两种说法都没有错。 关于async/await是Generator的语法糖: 所谓Generator语法糖,表明的就是aysnc/await实现的就是generator实现的功能。但是async/await比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。针对generator的这个缺点,后面提出了co这个库函数来自动执行next,相比于之前的方案,这种方式确实有了进步,但是仍然麻烦。而async配合await得到的就是断点执行后的结果。因此async/await比generator使用更普遍。

总结下来,async函数对 Generator函数的改进,主要体现在以下三点:

  • 内置执行器:Generator函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co函数库。但是,async函数和正常的函数一样执行,也不用 co函数库,也不用使用 next方法,而 async函数自带执行器,会自动执行。
  • 适用性更好:co函数库有条件约束,yield命令后面只能是 Thunk函数或 Promise对象,但是 async函数的 await关键词后面,可以不受约束。
  • 可读性更好:async和 await,比起使用 *号和 yield,语义更清晰明了。

关于async/await是Promise的语法糖: 如果不使用async/await的话,Promise就需要通过链式调用来依次执行then之后的代码:

function counter(n){
	return new Promise((resolve, reject) => { 
	   resolve(n + 1);
    });
}

function adder(a, b){
    return new Promise((resolve, reject) => { 
	   resolve(a + b);
    });
}

function delay(a){
    return new Promise((resolve, reject) => { 
	   setTimeout(() => resolve(a), 1000);
    });
}
// 链式调用写法
function callAll(){
    counter(1)
       .then((val) => adder(val, 3))
       .then((val) => delay(val))
       .then(console.log);
}
callAll();//5
复制代码

虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。 而async/await的出现,就使得我们可以通过同步代码来达到异步的效果

async function callAll(){
   const count = await counter(1);
   const sum = await adder(count + 3);
   console.log(await delay(sum));
}
callAll();// 5
复制代码

由此可见,Promise搭配async/await的使用才是正解!

总结

  • promise让异步执行看起来更清晰明了,通过then让异步执行结果分离出来。
  • async/await其实是基于Promise的。async函数其实是把promise包装了一下。使用async函数可以让代码简洁很多,不需要promise一样需要些then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。
  • async函数是Generator函数的语法糖。async函数的返回值是 promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。同时,我们还可以用await来替代then方法指定下一步的操作。
  • 感觉Promise+async的操作最为常见。因为Generator被async替代了呀~

Guess you like

Origin juejin.im/post/7082753409060716574