Explique en detalle las cuatro soluciones asíncronas de JS: función de devolución de llamada, Promesa, Generador, asíncrono/espera (lleno de productos secos)

¡Acostúmbrate a escribir juntos! Este es el primer día de mi participación en el "Nuggets Daily New Plan · April Update Challenge"

Conceptos sincrónicos y asincrónicos

Antes de hablar de estos cuatro esquemas asíncronos, aclaremos los conceptos de sincronización y asincronía:

La llamada sincronización (sincronización) , en términos simples, es una ejecución secuencial , lo que significa que solo se puede hacer una cosa al mismo tiempo, y la siguiente solo se puede hacer después de que se complete la ejecución actual. Por ejemplo, si vamos a la estación de tren a comprar boletos, si solo hay una ventanilla, entonces solo una persona puede comprar boletos al mismo tiempo y el resto debe hacer cola. Esta acción uno por uno es sincronización. La ventaja de la operación síncrona es que todo se ejecuta en secuencia y en orden, y no hay problema de que todos tomen un recurso al mismo tiempo. La desventaja de las operaciones sincrónicas es que bloquean la ejecución del código posterior . Si la tarea que se está ejecutando actualmente lleva mucho tiempo, los programas subsiguientes solo pueden esperar. Esto afecta la eficiencia. Correspondiente a la visualización de la página principal, puede causar el bloqueo de la representación de la página y afectar en gran medida la experiencia del usuario.

El llamado asíncrono (Asynchronization) , se refiere a que la ejecución del código actual no afecta la ejecución del siguiente código. Cuando el programa se ejecuta en el código asincrónico, el código asincrónico se colocará en la cola de tareas como una tarea, en lugar de insertarse en la pila de llamadas del subproceso principal. Después de ejecutar el subproceso principal, vaya a la cola de tareas para ejecutar la tarea correspondiente. Por lo tanto, la ventaja de la operación asíncrona es que no bloquea la ejecución del código posterior .

Escenarios de aplicaciones asíncronas en js

Al principio, hablé sobre los conceptos de sincronización y asincronía, entonces, ¿cuáles son los escenarios de aplicación de asincrónico en JS?

  • Tareas programadas: setTimeout, setInterval
  • Solicitud de red: solicitud ajax, carga de etiqueta img creada dinámicamente
  • Oyente de eventos: addEventListener

Cuatro formas de implementar la asincronía

Para escenarios asincrónicos como setTimeout, setInterval y addEventListener, no necesitamos implementar asincrónicos manualmente, solo llámelos directamente. Pero para las solicitudes ajax, la operación asíncrona de la base de datos en node.js, debemos implementarla nosotros mismos ~

1. Función de devolución de llamada

Antes de la aparición de las colas de microtareas, la principal forma en que JS lograba la asincronía era a través de funciones de devolución de llamada. Tome una solicitud Ajax simple como ejemplo, la estructura del código es la siguiente:

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);
            }
        }
    }
}

复制代码

Podemos llamar a la solicitud ajax de esta manera en el código comercial:

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

El método de éxito de ajax es una función de devolución de llamada, y la función de devolución de llamada ejecuta las operaciones adicionales que se realizarán después de que nuestra solicitud sea exitosa. De esta manera, inicialmente se realiza la asincronía, pero la función de devolución de llamada tiene una desventaja muy seria, es decir , el problema del infierno de devolución de llamada . Puedes imaginar, ¿qué pasa si iniciamos otra solicitud ajax en la función de devolución de llamada? ¿No sería necesario seguir escribiendo una solicitud ajax en la función de éxito? ¿Qué pasa si se requieren múltiples niveles de anidamiento para iniciar solicitudes ajax? ¿No requeriría múltiples niveles de anidamiento? Si el nivel de anidamiento es profundo, nuestra estructura de código puede volverse así: 回调地狱示意图por lo tanto, para resolver el problema del infierno de devolución de llamada, se proponen los conceptos de Promesa, async/await y generador.

2, promesa

Como una de las microtareas típicas, Promise puede hacer que JS logre el efecto de ejecución asíncrona. La estructura de una función Promise es la siguiente:

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') });
复制代码

Si necesitamos ejecutar código asíncrono anidado, en comparación con las funciones de devolución de llamada, las promesas se ejecutan como se muestra en el siguiente código:

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);
    });
复制代码

Es decir, para lograr el anidamiento de niveles múltiples ( llamadas en cadena ) hasta entonces, ¿parece esto más cómodo que las funciones de devolución de llamada?

El ciclo de vida por el que pasa cada Promesa es:

  • En curso (pendiente): la ejecución del código no ha finalizado en este punto, por lo que también se denomina no resuelto (no resuelto).
  • Resuelto: el código asincrónico ha terminado de ejecutarse. El código procesado entra en uno de dos estados:
    • 已完成(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替代了呀~

Supongo que te gusta

Origin juejin.im/post/7082753409060716574
Recomendado
Clasificación