上一篇文章 介绍 Promise的一系列概念和内部执行机制,这次我们看一下 JavaScript 中另外两个很重要的异步操作符 aysnc 和 await。
介绍aysnc 和 await 操作符之前,先看几个概念:
迭代器 (Itertor)
JavaScript中你肯定用过for...of、for...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 包含了很多可以迭代的对象,如: Set、Map、Array 等,而像 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 await 是 Promise + 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 中的 NewPromiseReactionJob 和 NewPromiseResolveThenableJob 任务创建机制
具体就是,我们知道async 函数不论结果如何都会返回一个 Promise 对象。但是,如果
async函数本身再返回的时候就返回了一个 Promise 对象,如何处理呢? 这一步依然会创建 一个包含等待内部状态改变的Promise的Promise,想一下上文的res(new Promise((resolve,rej)=>resolve("hello")))这种形式的Promise你就明白了,本质上也就是创建了两次微任务 导致2000的输出在ccc的后面。
最后想说一下: await 和 async 并非没有任何缺陷,错误捕获的处理有点不足,如下:
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)
}
可以捕获异步操作中的异常
但是如果异常发生在异步操作的外部:
const bar = ()=>{
throw new Error('poos')
return new Promise((res,rej)=>{
res(1)
})
}
await bar().catch(e=>console.log("require",e))
则不能使用 catch 函数直接捕获异常
此时则必须使用 try...catch 表达式进行异常的捕获,如果代码中的 await async操作过多,且你想获取每一个具体的报错信息,则可能需要使用自行封装一种带有try...catch 的 async await 异步操作的类库,较为妥善。