🔥另一种角度解读JavaScript中的Promise、async和await 异步操作 (下)🔥

185 阅读4分钟

      上一篇文章 介绍 Promise的一系列概念和内部执行机制,这次我们看一下 JavaScript 中另外两个很重要的异步操作符 aysncawait

       介绍aysncawait 操作符之前,先看几个概念:

迭代器 (Itertor)

       JavaScript中你肯定用过for...offor...in,类似的 Api 并不具有高可控的能力(如控制暂停、随时获取元素),要完成这些功能,就需要使用迭代器进行控制,我们可以自定义一个获取数组的迭代器,例如:


const list = ['a', 'b', 'c', 'd', 'e'];  //遍历的数组

let index = 0; 
const _iterrator = { 
  next: function () {  //迭代的方法,控制每一步骤的执行
    if (index < list.length) { 
     // 返回一个对象,对包含数组的元素和是否遍历完成的信息
      return { done: false, value: list[index++] }; 
    }
    //遍历完成,done表示遍历完成,value的值为undefined
    return { done: true, value: undefined };
  },
};

console.log(_iterrator.next()); //{ done: false, value: 'a' }
console.log(_iterrator.next());//{ done: false, value: 'b' }
console.log(_iterrator.next());//{ done: false, value: 'c' }
console.log(_iterrator.next());//{ done: false, value: 'd' }
console.log(_iterrator.next());//{ done: false, value: 'e' }
console.log(_iterrator.next());//{ done: true, value: undefined }

我们也可以使用 Array.prototype[@@iterator]() 自带的迭代器方法,如下:

const arr = ["a", "b", "c", "d", "e"];
const arrIter = arr[Symbol.iterator]();  //自带的迭代器方法,下文会有讲解
console.log(arrIter.next().value); // a
console.log(arrIter.next().value); // b
console.log(arrIter.next().value); // c
console.log(arrIter.next().value); // d
console.log(arrIter.next().value); // e
console.log(arrIter.next().value); // undefined

      注意:JavaScript 包含了很多可以迭代的对象,如: SetMapArray 等,而像 Object 对象类型不具有迭代器属性 ([Symbol.iterator]),这也就是它无法直接使用 for..of 的原因。

我们现在为 Object 对象实现一下Symbol.iterator 属性:

const iterator = {
  list: ['a', 'b', 'c', 'd'],
//只要是实现了 [Symbol.iterator]方法就可以迭代
  [Symbol.iterator]: function () { 
    let index = 0;
    return {
      next: () => {
        if (index < this.list.length) {
          return { done: false, value: this.list[index++] };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  },
};
const iterator1 = iterator[Symbol.iterator]();

console.log(iterator1.next()); //可以使用next方法

for (let item of iterator) {   //也可以用for of 方法
  console.log(item);
}

生成器 (generator)

      可以看到上面实现迭代器的核心方法是 next 但是每次手动实现较为复杂,生成器 generator 就是为了实现更为简单的使用迭代器。例子如下:


//函数的后面接一个*,表示是一个生成器函数
function* foo() {
  console.log('start');

  let value1 = 200;
  console.log('1', value1);
  yield value1;  //yield表示,next在哪一行停止,可以理解为打了一个断点,用next执行下一个断点

  let value2 = 300;
  console.log('2', value2);
  yield value2;

  let value3 = 500;
  console.log('3', value3);
  yield value3;

  console.log('end');
  return 'end';
}

const fo = foo();
console.log(fo.next()); 
console.log(fo.next());  
console.log(fo.next());
console.log(fo.next());  
console.log(fo.next()); 

output>>> start
1 200
  {value: 200, done: false}
2 300
  {value: 300, done: false}
3 500
  {value: 500, done: false}
end
  {value: 'end', done: true}
  {value: undefined, done: true}

;注意:生成器也可以传参,生成器的 next 还可以传入参数,传入的参数在 yeild 的返回值中 如:

function* foo(num) {
  console.log('start');
  const value1 = 100 * num;
  console.log('1', value1);
  const n = yield value1;

  const value2 = 200 * n;
  console.log('2', value2);
  const m = yield value2;

  const value3 = 300 * m;
  console.log('3', value3);
}

const iterator = foo(9);
//next传的参数在yield返回值中
console.log(iterator.next()); // {value: 900, done: false}
console.log(iterator.next(33));  // {value: 6600, done: false}
console.log(iterator.next(44));  //{value: undefined, done: false}

使用生成器来替代迭代器使用:

function* _iterator(arr) {

  //写法三:yeild* 后面跟上一个可迭代对象
  yield* arr;  // **表达式**用于委托给另一个`generator` 或可迭代对象, 比如说数组、字符串、`arguments` 对象等等。
}

const co = _iterator([1,2,3]
co.next()  //{value: 1, done: false}
co.next()  //{value: 2, done: false}
co.next()  //{value: 3, done: false}
co.next()  //{value: undefined, done: true}

async await

       async await是用来做异步请求的,解决了之前多次函数回调地狱以及 Promise 不简洁的链式调用问题。实际上 async awaitPromise + generator 的语法糖,使用 generator 调用值,并把值包装为 Promise 对象。

  • async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise
  • await 操作符用于等待一个 Promise 兑现并获取它兑现之后的值。它只能在异步函数或者模块顶层中使用。

await 对执行顺序的影响

       当函数执行到 await 时,被等待的表达式会立即执行,所有依赖该表达式的值的代码会被暂停,并推送进微任务队列(microtask queue)。然后主线程被释放出来,用于事件循环中的下一个任务。即使等待的值是已经敲定的 promise 或不是 promise ,也会发生这种情况。

先看一道简单的输出题:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");

setTimeout(function() {
  //  setTimeout放入event-loop中的macro-tasks队列,暂不执行
  console.log("setTimeout");
}, 0);

async1();

new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise end");
});
console.log("script end");


// 输出如下
script start
async1 start
async2
promise1
script end
async1 end
promise end
setTimeout

有点简单了? 那就再看一道:

async function song() {
    return new Promise((res, rej) => {
        console.log(111)
        res(2000)
    })
}
async function test() {
        const p1 = await song();
        console.log(p1);
}
Promise.resolve().then(res => {
    console.log('aaa')
}).then(res => {
    console.log('bbb')
}).then(res => {
    console.log('ccc')
}).then(res => {
    console.log('ddd')
})

test();

console.log("pre_load")




output: 111  pre_load  aaa   bbb  ccc  2000  ddd

以上代码,绝大部分人是不清楚先后执行顺序,你可以先想想为什么输出是这样。

解析时间:

      有一部分人觉得输出应该是: 111 pre_load aaa bbb 2000 ccc ddd,其实知道输出是这个,说明你对 js 的任务队列执行掌握的不错,那为什么不是这个顺序,这里我们直接给出答案,就是上期所说的 Promise 中的 NewPromiseReactionJobNewPromiseResolveThenableJob 任务创建机制

      具体就是,我们知道async 函数不论结果如何都会返回一个 Promise 对象。但是,如果 async 函数本身再返回的时候就返回了一个 Promise 对象,如何处理呢? 这一步依然会创建 一个包含等待内部状态改变的 PromisePromise ,想一下上文的res(new Promise((resolve,rej)=>resolve("hello"))) 这种形式的 Promise 你就明白了,本质上也就是创建了两次微任务 导致 2000 的输出在 ccc 的后面。

最后想说一下: awaitasync 并非没有任何缺陷,错误捕获的处理有点不足,如下:

const bar = ()=>{

    return new Promise((res,rej)=>{
        throw new Error('poos')
})
}
await bar().catch(e=>console.log("require",e))

或者直接使用try...catch

try{
    await bar()
    
}catch(e){
    console.log("require",e)
}

可以捕获异步操作中的异常 image.png

但是如果异常发生在异步操作的外部:

const bar = ()=>{
 throw new Error('poos')
    return new Promise((res,rej)=>{
      res(1)
})
}
await bar().catch(e=>console.log("require",e))

则不能使用 catch 函数直接捕获异常

image.png

      此时则必须使用 try...catch 表达式进行异常的捕获,如果代码中的 await async操作过多,且你想获取每一个具体的报错信息,则可能需要使用自行封装一种带有try...catch async await 异步操作的类库,较为妥善。