1. 开始
关于async和await的定义,下面分别是MDN和Ai的解释:
MDN:
async function 声明创建一个绑定到给定名称的新异步函数。函数体内允许使用 await
关键字,这使得我们可以更简洁地编写基于 promise 的异步代码,并且避免了显式地配置 promise 链的需要。
Ai:
async/await 是 JavaScript 异步编程的终极简化方案,通过隐藏 Promise 的链式调用细节,让开发者专注于业务逻辑的线性表达,其实现原理基于Promise和生成器(generator)。
上面提到了一些关键词,异步编程、链式调用、Promise和生成器,也提到了async/await的实现原理是基于Promise和生成器。
对于我而言前面的几个关键词都很熟悉,针对生成器似乎有点陌生,什么是生成器?async/await和生成器又有什么关系呢?
2. 前置:Generator 函数
2.1. 特征
上面提到的生成器,也就是Generator函数:Generator - JavaScript | MDN, 它是ES6 提供的一种异步编程解决方案。
下面是Generator函数的一些特征:
- 使用function关键字定义:Generator函数使用function*关键字定义,而不是普通的function关键字。
- 可以暂停和恢复执行:Generator函数可以在执行过程中通过yield关键字来暂停执行,并在需要时恢复执行。
- 可以通过调用生成器对象的next()方法来逐步执行:Generator函数返回一个生成器对象,通过调用生成器对象的next()方法,可以逐步执行Generator函数中的代码。每次调用next()方法时,Generator函数会从上次暂停的地方继续执行,直到遇到下一个yield关键字或函数结束。
- yield关键字可以返回值:yield关键字可以返回一个值,这个值会成为生成器对象的next()方法返回的对象的value属性。
2.2. 函数执行方式
关于Generator 函数,下面给出一个简单demo:
function* gen() {
yield "hello";
yield "world";
yield "!";
}
let g = gen();
console.log("[ g ] >", g);
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());
console.log("g.next()", g.next());
// 下面是运行结果
[ g ] > Object [Generator] {}
g.next() { value: 'hello', done: false }
g.next() { value: 'world', done: false }
g.next() { value: '!', done: false }
g.next() { value: undefined, done: true }
g.next() { value: undefined, done: true }
与普通函数不同的是Generator 函数调用后并不会直接执行,而是返回一个迭代器对象(Generator Object,同样这个迭代器对象可以被for of遍历消费是Symbol.iterator
方法的最简单实现)
Generator函数的执行是通过调用迭代器对象示例上的方法进行控制,下面是迭代器对象原型上的一些方法:
[[Prototype]]: Generator
constructor:GeneratorFunction {prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction'}
next: ƒ next()
return: ƒ return()
throw: ƒ throw()
Symbol(Symbol.toStringTag): "Generator"
[[Prototype]]: Object
通过调用next方法,依次显示 3 个yield
表达式的值,第四次调用next方法时Generator 函数已经运行完毕,next
方法返回对象的value
属性为undefined
,done
属性为true
。以后再调用next
方法,返回的都是这个值。
2.3. next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
下面有一段这样的代码:
function* gen() {
let result1 = yield "hello";
console.log('[ gen函数内部打印 result1 ] >', result1)
let result2 = yield "world";
console.log('[ gen函数内部打印 result2 ] >', result2)
let result3 = yield "!";
console.log('[ gen函数内部打印 result3 ] >', result3)
}
let g = gen();
console.log('[ g.next(1) ] >', g.next(1))
console.log('[ g.next(2) ] >', g.next(2))
console.log('[ g.next(3) ] >', g.next(3))
console.log('[ g.next(4) ] >', g.next(4))
// 下面是运行结果
[ g.next(1) ] > { value: 'hello', done: false }
[ gen函数内部打印 result1 ] > 2
[ g.next(2) ] > { value: 'world', done: false }
[ gen函数内部打印 result2 ] > 3
[ g.next(3) ] > { value: '!', done: false }
[ gen函数内部打印 result3 ] > 4
[ g.next(4) ] > { value: undefined, done: true }
代码中一共执行了四次g.next()入参分别是1-4,gen函数内部是这样执行的:
- 执行g.next(1):此时控制台打印第一个yield表达式的值,即“hello”,函数内的代码暂停执行
- 执行g.next(2):函数内的代码继续执行,在第2行打印了result1,由于本次执行的next方法传入了一个参数“2”,这个“2”会被当作上一个yield表达式的值赋给result1,所以在gen函数内部打印出来的result1的值为“2”,继续执行第3行代码,控制台打印第二个yield表达式的值,即“world”,函数内的代码暂停执行
- 执行g.next(3):函数内的代码继续执行,在第4行打印了result2,由于本次执行的next方法传入了一个参数“3”,这个“3”会被当作上一个yield表达式的值赋给result2,所以在gen函数内部打印出来的result2的值为“3”,继续执行第5行代码,控制台打印第三个yield表达式的值,即“!”,函数内的代码暂停执行
- 执行g.next(4):gen函数走到了第6行,但是这个next方法传入了参数4,这个参数4被当作上一个yiled表达式的返回值即result3的值,所以在gen函数内部打印result3为4,后续没有yield表达式,控制台打印undefined,函数代码执行结束
3. Generator 函数应用:异步操作的同步化表达
async/await最重要的特点就是能够将异步代码同步化, 上面举例的代码yield表达式后面跟的都是同步代码,那么如果后面跟异步代码会是怎么样的?
3.1. 场景
我们从案例切入,假设现在我们现在需要实现一个方法initData() 方法,在方法内部需要依次调用两个异步接口,其中需要等待第一个接口的数据返回后,再拿第一个接口的数据调用第二个接口。
3.2. 模拟请求
首先我们定义一个模拟请求的request方法,后续的代码使用这个方法模拟异步请求。
const request = (data = {}, duration = 500) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...data, _date: Date.now() });
}, duration);
});
};
3.3. initData()方法实现
按照上面的经验,我们的第一版代码实现如下:
const initData = function* () {
console.log("showLoading");
let response = yield request();
console.log("[ response ] >", response);
let response2 = yield request(response._date);
console.log('[ 最终数据 ] >',response2)
console.log("hideLoading");
};
let g = initData()
console.log(g.next())
// 下面是运行结果
showLoading
{ value: Promise { <pending> }, done: false }
我们理想的情况应该是走完initData方法的全部代码,但是由于next方法只执行了一次,就导致initData方法中的代码只能运行到第2行,所以这里我们还需要手动调用next方法来控制函数的执行:
let g = initData();
let result = g.next();
// 手动调用next方法
result.value.then((res) => {
let result = g.next(res)
result.value.then((res) => {
g.next(res);
});
});
// 下面是运行结果
showLoading
[ response ] > { _date: 1742978765222 }
[ 最终数据 ] > { _date: 1742978765724 }
hideLoading
这样我们initData中的代码就能像同步代码一样运行了,如果我们忽略手动调用的代码,同时把*想成async,yield想成await,基本就和async/await的基本用法一样了。
4. async/await的简单实现
实现自动执行器,无非就是实现Generator函数的自动流程管理,使我们不需要手动调用next方法。
4.1 同步的自动执行器
最简单的方法是使用while循环:
const initData = async () => {
console.log("showLoading");
let response = await request();
console.log("[ response ] >", response);
let response2 = await request(response._date);
console.log("[ 最终数据 ] >", response2);
console.log("hideLoading");
};
// 实现自动执行器函数
const spawn = (generatorFn) => {
let g = generatorFn();
let result = g.next();
while (!result.done) {
result = g.next(result.value);
}
};
spawn(initData)
// 下面是执行结果
showLoading
[ response ] > Promise { <pending> }
[ 最终数据 ] > Promise { <pending> }
hideLoading
上面代码中,Generator 函数initData
会自动执行完所有步骤。
但是,这也仅仅适合同步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。
4.2 异步的自动执行器
对于异步操作,我们需要借助Promise:
const asyncSpawn = (generatorFn) => {
return new Promise((resolve) => {
let g = generatorFn();
const next = (data) => {
let result = g.next(data);
if (result.done) return resolve(result.value);
if (result.value instanceof Promise) {
result.value.then((response) => {
next(response);
});
} else {
next(Promise.resolve(result.value));
}
};
next();
});
};
- 第一步,initData开始执行。
- 第二步,initData执行到异步操作,进入暂停,执行权转移到Promise的回调中。
- 第三步,(一段时间后)Promise状态变为resolved,执行then方法回调中的g.next()交还执行权。
- 第四步,initData恢复执行。
上面代码中,只要 Generator 函数还没执行到最后一步,next
函数就调用自身,以此实现自动执行。
最后,我们只需要关注initData中的代码实现,将流程控制交个asyncSpawn函数即可。
const initData = function* () {
console.log("showLoading");
let response = yield request();
console.log("[ response ] >", response);
yield 123
let response2 = yield request(response._date);
console.log("[ 最终数据 ] >", response2);
console.log("hideLoading");
return "end"
};
asyncSpawn(initData).then((response)=>{
console.log("[ response ] >", response);
})
// 执行结果
showLoading
[ response ] > { _date: 1743065649287 }
[ 最终数据 ] > { _date: 1743065649790 }
hideLoading
[ response ] > end