手写 await 实现:迭代器、生成器与异步编程的完美结合
一、什么是迭代器(iterator)
背景
在以往的JS代码中,想要遍历不同的数据类型往往都要使用不同的方式:
- 数组需要使用经典的 for 循环进行遍历
- 对象需要使用 for in 循环遍历,或者使用 Object.keys() 转换为数组后进行遍历
- 对于开发者自行定义的一些数据结构,想要对象实现遍历操作也需要开发者自行为其提供遍历方式
- 后续可能会拓展更多数据类型,这些数据类型都需要方式去遍历
这些问题就导致了初学者在学习 JavaScript 时,往往需要背下来很多数据结构的遍历方式,代码的复用性也随之降低。
于是在 ES6 语法中就引入了 迭代器 的概念。迭代器 为不同的数据结构提供了相同的遍历方式,同时还基于此拓展了很多新语法,例如解构赋值等。
迭代器的概念与使用
迭代器 其实本质上就是一个对象,对象上定义了一些方法,其中就包括了 next() 方法,该方法返回一个包含两个属性的对象:
value: 当前元素的值done: 布尔值,表示遍历是否结束
我们把迭代器对象的生成函数通过 [Symbol.iterator] 属性挂载在一个对象上,此时该对象就成为了一个 可迭代对象。常见的可迭代对象包括:字符串、数组、Map、Set等,官方确实为这些常用的内置数据结构都定义好了迭代器。
Symbol.iterator 是一个表达式,这是一个预定义好的、类型为 Symbol 的特殊值。通常情况下,我们会将一个数据的迭代器对象生成函数定义在其
Symbol.iterator属性上,这就意味着我们可以通过array[Symbol.iterator]去访问一个数组的迭代器生成函数。
接下来我们尝试去使用数组的迭代器,那么第一步就是要通过迭代器生成函数生成迭代器,随后我们可以使用迭代器上的 next 方法进行遍历:
const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
实际上,for...of 循环本质上是通过调用对象的 [Symbol.iterator]() 方法来获取迭代器,然后重复调用迭代器的 next() 方法,直到 done 属性变为 true。
由于我们之前说过,任何实现了迭代器方法并且挂载在了 [Symbol.iterator] 属性上的对象都称之为可迭代对象,这就意味着我们可以自己定义可迭代对象,比如实现平方数的集合:
class SquareNumber {
[Symbol.iterator]() {
let i = 0;
return {
next: () => {
return { value: i * i, done: i++ > 10 };
},
};
}
}
const square = new SquareNumber();
for (let i of square) {
console.log(i); // 0 1 4 9 16 25 36 49 64 81 100
}
二、什么是生成器(generator)
上文我们已经提到了迭代器,它是一种需要手动实现 next() 方法的对象。而 生成器 本质上则是迭代器的语法糖,可以自动生成 next() 方法,同时自动满足迭代器协议。
生成器可以通过 function* 来定义,就像定义一个函数一样。生成器有一个特别强大的功能,就是 可以在其中使用 yield 关键字 来暂停函数的执行。 由于其满足迭代器协议,这就意味着其执行后的结果也会提供 next() 方法和 done 属性,我们可以通过这两个内容来控制函数的执行和暂停。
function* myGenerator() {
console.log('第一次执行')
const a = yield 1 + 1;
console.log('第二次执行', a)
const b = yield 2 * 4;
console.log('第三次执行', b)
}
const gen = myGenerator()
console.log(gen.next()) // 第一次执行,{ value: 2, done: false }
console.log(gen.next()) // 第二次执行 undifined,{ value: 8, done: false }
console.log(gen.next()) // 第三次执行 undifined,{ value: undefined, done: true }
根据上面的例子我们看到,当生成器刚被执行的时候得到 gen 的时候,函数并不会执行,随后每次执行 gen.next() 时都会使生成器内的代码执行到下一个 yield 处。
并且我们可以发现,每次执行 gen.next() 时都会把 yield 关键字后的表达式结果作为 value 字段导出出来,同时还会导出一个 done 字段。
需要注意的是,凡是用到在赋值表达式的右侧使用了 yield,其左侧的变量都不会被正常赋值,所以我们在生成器里打印的内容中存在 undifined。
yield 会暂停生成器函数的执行,导致赋值操作无法立即完成。具体来说:当执行到 yield 表达式时,生成器会立即返回右侧的值(暂停执行),不完成当前行的赋值操作。
我们可以通过在外部代码再次调用 next() 时,把上一次调用 next() 得到的 value 传递进参数里,此时上一个 yield 表达式整体会被替换为 next() 传入的值,从而顺利完成赋值操作。
function* myGenerator() {
console.log('第一次执行')
const a = yield 1 + 1;
console.log('第二次执行', a)
const b = yield 2 * 4 + a;
console.log('第三次执行', b)
}
const gen = myGenerator()
console.log(gen.next()) // 第一次执行,{ value: 2, done: false }
console.log(gen.next(666)) // 第二次执行 666,{ value: 674, done: false }
console.log(gen.next(4)) // 第三次执行 4,{ value: undefined, done: true }
从上面的例子中可见,我们在第一次执行 next() 后得到了结果 2,这是第一个 yield 关键字后面的表达式的执行结果。第二次执行 next() 时传入了 666,可以发现直接将 a 赋值为了 666,这就说明 无论 yield 后面的结果是什么,next() 中传递的参数都能直接将这个结果覆盖掉。
三、通过生成器模拟 await 功能
背景与原理
在了解迭代器和生成器的概念后,我们可以发现生成器有一个非常强大的能力:它可以在 yield 关键字处暂停执行,等待外部调用 next() 方法继续执行。这个特性让我们可以用同步的写法来处理异步操作,这就是 async/await 语法的核心思想。
async/await 的底层实现就是基于生成器的。我们可以通过生成器来模拟 await 的功能。
核心思路
- 使用
function*定义生成器函数,在需要等待异步操作的地方使用yield, 后面跟的是返回 Promise 的异步函数 - 编写一个
run函数来自动处理生成器的执行,当遇到 Promise 时等待其完成 - 如果一次 Promise 完成后,返回的 done 属性为 false,根据返回的 done 属性决定是否要递归调用
run函数 - 递归调用时将上一次Promise返回的值,传递给
next()
实现代码
function* asyncFunc() {
console.log("asyncFunc start");
let result1 = yield asyncFunc1();
console.log("result1", result1);
let result2 = yield asyncFunc2(result1);
console.log("result2", result2);
return result2;
}
function asyncFunc1() {
return new Promise((resolve) => {
setTimeout(() => resolve("AsyncFunc1 Done"), 1000);
});
}
function asyncFunc2(arg) {
return new Promise((resolve) => {
setTimeout(() => resolve(`AsyncFunc2 Done with ${arg}`), 1000);
});
}
const generator = asyncFunc();
function run(generator, tempResult) {
const result = generator.next(tempResult);
if (result.value instanceof Promise) {
// 如果result.value是Promise,则等待Promise完成
result.value
.then((res) => {
run(generator, res);
})
.catch((err) => {
console.log("err", err);
});
} else if (result.done) {
// 如果result.done为true,则返回result.value
return result.value;
} else {
// 如果result.value不是Promise,则直接执行下一个yield
run(generator, result.value);
}
}
run(generator);
// asyncFunc start
// 1000毫秒 —— AsyncFunc1 Done
// 2000毫秒 —— AsyncFunc2 Done with AsyncFunc1 Done
让我们看一下上面这段代码的执行过程:
第一次调用 run(generator):
generator.next()执行到第一个yield asyncFunc1()- 返回
{ value: Promise, done: false } - 等待 Promise 完成(1秒后)
第一个 Promise 完成后:
- 调用
run(generator, "AsyncFunc1 Done") result1被赋值为"AsyncFunc1 Done"- 执行到第二个
yield asyncFunc2(result1) - 返回新的 Promise
第二个 Promise 完成后:
- 调用
run(generator, "AsyncFunc2 Done with AsyncFunc1 Done") result2被赋值- 生成器执行完毕,返回最终结果
上面这段代码示例中展示的只是理想情况下的场景,因为我们每次执行 Promise 时都会得到正确的返回。在实际开发过程中,异步函数可能会出现异常,而我们现在的代码示例中不具备抛出错误的能力。
除了 next() 方法,生成器还为我们提供了 throw() 方法。该方法允许从外部向生成器内部抛出错误,我们可以借助该方法正确处理 Promise 内部的异常并继续向外抛出。
function* asyncFunc() {
try {
console.log("asyncFunc start");
let result1 = yield asyncFunc1();
console.log("result1", result1);
let result2 = yield asyncFunc2(result1);
console.log("result2", result2);
return result2;
} catch (error) {
console.log("error", error);
}
}
function asyncFunc1() {
return new Promise((resolve, reject) => {
setTimeout(() => reject("AsyncFunc1 Error"), 1000);
});
}
function asyncFunc2(arg) {
return new Promise((resolve) => {
setTimeout(() => resolve(`AsyncFunc2 Done with ${arg}`), 1000);
});
}
const generator = asyncFunc();
function run(generator, tempResult) {
const result = generator.next(tempResult);
if (result.value instanceof Promise) {
// 如果result.value是Promise,则等待Promise完成
result.value
.then((res) => {
run(generator, res);
})
.catch((err) => {
generator.throw(err);
});
} else if (result.done) {
// 如果result.done为true,则返回result.value
return result.value;
} else {
// 如果result.value不是Promise,则直接执行下一个yield
run(generator, result.value);
}
}
run(generator);
// asyncFunc start
// 1000毫秒 —— AsyncFunc1 Error
// error AsyncFunc1 Error
四、总结
通过本文的学习,我们从 JavaScript 的迭代器开始,逐步深入到了生成器的概念,最终模拟了 async/await 语法的实现。实际上,理解这些底层机制可以帮助我们更深入的理解 async/await 语法,同时也让我们更好的理解 ES6 提供给我们的新语法和异步编程的新范式。