前言
异步是JavaScript中的重要内容,异步的主要作用是把耗时间的代码“放一边”,让其不阻塞同步代码,等到异步代码出结果了,再通过回调函数来处理其结果。但是在实际工作中使用异步的时候有一个重要的问题,即多个异步代码的顺序问题。
假如我们要做多个Ajax请求,第二个请求的参数是一个请求的结果,这就要求异步代码之间需要有序,保证第二个请求在第一个请求执行完之后再执行。其实细考虑起来这是个略显诡异的需求,也就是使异步代码“同步化”。不过ES6的许多新特性使得这一操作变得更为简化。我们可以用Promise,Generator,或async/await分别实现上述“同步化异步代码”的需求。三者对照,可以更好地将ES6中的异步理解。
话不多说,上需求。首先模拟一个取数据的异步操作,返回的数据类型是Promise(比较典型的用法),代码如下
function getData(params) {
return new Promise((resolve) => {
// 模拟ajax
setTimeout(() => {
resolve("result: " + params);
}, 500);
});
}
我们的需求是,先用getData取到数据,再对取到的数据进行作一次getData,两次操作需要有序,不然第二次的参数就空了。------以上是实习面试腾讯时的一道题,当时初涉前端,连题都没搞懂😭。
后面将用三种方法实现这个需求,领略ES6异步三境界。
一重境界:用Promise使异步代码有序
Promise是为异步而生的,用.then可以大大缓解以前“回调地狱”的情况。直接上代码,详情见注释。
getData("start").then((res) => {
// 取到结果 result: start
console.log("stage1", res);
getData(res).then((res) => {
// 取到结果 result: result: start
console.log("stage2", res);
console.log("all Done!!");
})
})
打印结果如下:
stage1 result: start
stage2 result: result: start
all Done!!
如上所见,简单易懂,但是多次异步写起来还是有点难受,“回调地狱”的问题没有彻底解决。不过没关系,后面还有新办法。
二重境界:用async/await简化Promise
其实async/await可以算是上述问题的最终形态,本来应该放在最后说的,但相比Generator而言,async/await更好简洁,也更常用,于是便放在第二重了,毕竟Generator理解起来确实有点费劲。
async/await两兄弟,一个一个来介绍。
async:放于某函数function关键字之前时,可以将该函数的返回包裹上一个成一个fulfilled状态的Promise对象。看代码:
async function get1() {
return 1;
}
get1(); // 返回结果为Promise {<fulfilled>: 1}
await:你async能包一个promise,我await就能解开一个promise😊。上面代码别删,接着用。
await get1(); // 结果为 1
两兄弟最简单的功能是对立的,但二者结合起来却能轻松地完成“异步代码同步化”的问题。先上代码:
// async/await 写法,
async function asy_fn() {
// get到数据之前,下行代码下面的其他代码都得等着(wait)
let stage1 = await getData("start");
console.log("stage1", stage1);
let stage2 = await getData(stage1);
console.log("stage2", stage2);
return "all Done!!";
}
let res_asy = asy_fn();
// 因为返回值是Promise,所以最后的结果要then一下再打出来
res_asy.then((res) => {
console.log(res)
})
下面讲点细节:
- await只能在async function中起到上面的作用,而且await右边函数的返回值得是promise,setTimeout造成的异步await无效。
- await暂停代码时,其左边的赋值也是未进行的,实际上是停在了await右边的函数里。
- 因为await产生的异步效应是和promise同级别的,也就是微任务级别。具体细节可见《反复横跳的await与Promise优先级》。
三重境界:用Generator和Promise解构async/await
Generator(生成器or迭代器?)是个陌生的家伙,一般业务中用到的比较少(写出来同事看不懂咋办😂),但是他是async/await背后的原理。而且若是能灵活掌握这个知识点的话,可以造出很多东西。本文就把这个当作最后boss了,用这个来理解async/await。
先简述,Generator,通过function*这样的函数声明方式产生一个迭代器函数,执行一下迭代器就产生一个迭代器对象。这个函数里的yield和return是一个个节点,在迭代器上next一下就从一个节点执行到下一个节点。就是会停止,对了,这就是await会停在某行代码上的原理😏。
show me your code,示例如下:
function* gene() {
yield 1;
yield 2;
return 3;
}
let g = gene(); // 生成一个迭代器对象
console.log(g.next());
// { value: 1, done: false }
console.log(g.next());
// { value: 2, done: false }
console.log(g.next());
// { value: 3, done: true }
next()产生的结果是{value, done},这个对象里的值value是yield右侧的值或函数返回值,done是布尔值,迭代器是否执行完,即后面是否有其他的yield或return语句。
感觉next方法差不多整明白了,其实并没有😏。next函数其实可以传参数,next函数的参数即为上一个yield表达式的返回值(有点绕)。示例如下:
function* gene() {
let n1 = yield 1;
console.log(n1);
let n2 = yield 2;
console.log(n2);
return 3;
}
let g = gene();
console.log(g.next());
console.log(g.next("res for yield 1"));
// yield 函数返回值是下一个next运行时的输入值,
// 每次.next都执行到yield及后面的部分为止,
// 前面的赋值和yield本身都不返回值
console.log(g.next("res for yield 2"));
有点难理解,毕竟在下面执行的next里的参数跑到上面去了。但我们把每个next当作独立函数的话会更好理解。在执行到第二个next时,执行的代码其实是:
// console.log(g.next("res for yield 1"));
// 相当于
function next(param) {
let n1 = param;
console.log(n1);
yield 2;
}
以块为单位来理解yeild返回值便会比较清晰。
科普完毕,圆规正转,如何用Generator实现“异步代码同步化”。上代码:
function* gen() {
let stage1 = yield getData("start");
console.log("stage1", stage1);
let stage2 = yield getData(stage1);
console.log("stage2", stage2);
return "all Done!!";
}
function run(generator, v) {
let { value, done } = generator.next(v);
if (!done) {
value.then((res) => {
run(generator, res);
});
} else {
console.log(value);
}
}
let res = run(gen());
先实现一个gen迭代器,生成一个迭代器实例放在run里自动化地跑,毕竟一个一个地写next也太二了。
发现没有,现在的用生成器实现方式已经很接近async/await的实现方式了,不过async函数返回的是promise,run返回的是字符串。稍作改变就可以达到相同的效果。所以下面代码就是“如何用Generator和Promise模拟async/await?”,即标题async/await = Promise + Generator。
function* gen(stage0) {
let stage1 = yield getData("start");
console.log("stage1", stage1);
let stage2 = yield getData(stage1);
console.log("stage2", stage2);
return "all Done!!";
}
function run(generator, v) {
let { value, done } = generator.next(v);
return new Promise ((resolve, reject) => { // 包个promise
if (!done) {
value.then((res) => {
return run(generator, res).then(resolve);
});
} else {
return resolve(value); // gen函数return时的处理
}
});
}
let res = run(gen());
res.then((res) => {
console.log(res)
})
上述代码可与二重境界中代码等价,这便是async/await背后的实现原理了。于是就有了这种说法。
async/await是一个promise+generator+run函数的语法糖。
后记
ES6异步Generator使用较少,但这又是理解async/await的关键一步,填上这个空缺,认知才够完整。