最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。
前言
作为一个前端工程师,相信大家都已经体验到了由ES7的async/await带来的好处了。可以说它彻底的改变了我们写代码的风格方式。今天我们就一起来深入一下,这个async/await的前世今生。
用法
async/await的用法相信大家都已经很熟了,这里就只作简单的描述。
- async是一个关键字,作用是声明某个函数是async函数。
- await必须写在async函数中。
- async函数中的代码会逐行运行
- await后可以跟一个promise实例,当promise的状态改变await才会结束。
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}
asyncCall();
意义
可以看出,async/await彻底解决了回调写法的问题。用同步的代码风格,实现回调的逻辑内容。让代码可读性更高。
而其实在此之前,我们在处理回调问题的过程中经历了很多个阶段。在最早的时候,如果希望有序地执行回调函数,就必须在回调中嵌套回调。如下代码:
var sayhello = function (name, callback) {
setTimeout(function () {
console.log(name);
callback();
}, 1000);
}
// 希望first second third 有序执行,就必须让他们在回调中嵌套。
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
而es6中我们迎来了promise,他通过返回新的promise实例的方式。让我们可以不停的then,这样就解决了回调地狱的嵌套。参考如下:
var sayhello = function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(name);
resolve(); //在异步操作执行完后执行 resolve() 函数
}, 1000);
});
}
sayhello("first").then(function () {
return sayhello("second"); //仍然返回一个 Promise 对象
}).then(function () {
return sayhello("third");
}).then(function () {
console.log('end');
}).catch(function (err) {
console.log(err);
})
但这样我们又掉进了另一个痛苦中,就是如果then的次数太多,同样会让代码的可读性越来越差。开发者希望追求的是一种,写起来像同步代码那样逻辑自然流程的代码风格。因此ES7的async/await彻底解决了这个问题。
var sayhello = function (name) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(name);
resolve(); //在异步操作执行完后执行 resolve() 函数
}, 1000);
});
}
var main = async function(){
try{
await sayhello("first");
await sayhello("second");
await sayhello("third");
await sayhello('end');
}catch(err){
console.log(err);
}
}
main();
还有多少人记得generator?
其实在上方描述的历史演变中,我们遗漏了ES6的generator了。generator是JavaScript对同步风格的一次尝试,也是async/await的原型。可以说async/await其实是generator的语法糖,在async/await出来之后,generator已经很少人用了。但在早期版本的cnpm和koa1中,我们都可以很容易找到generator的身影。
使用
function* 这种声明方式(function关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。而在生成器函数中,我们可以使用yield语句。
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }
我们发现生成器函数中的内容是可以暂停的,当函数中的代码运行到yield语句之后,就会暂停。只有当generator调用next之后,函数才会继续往下走。而generator每次调用next,都会返回一个对象,里面有由yield返回的value,和说明生成器函数是否执行到底了标识done。
如果希望在生成器函数执行到底之后返回的不是undefined,可以在函数底部return一个值。
function* gen() {
yield 1
yield 2
return 3
}
const g = gen()
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传参
处理在generator调用next之后获得由生成器函数传递出来的值以外。在调用next时,也可以往生成器函数中传递值。写法参考以下代码:
function* gen() {
var num1 = yield "from fun: 1";
console.log(num1);
var num2 = yield "from fun: 2";
console.log(num2)
return "from fun: 3"
}
const g = gen()
console.log(g.next("from outside 1")) // { value: "from fun: 1", done: false }
console.log(g.next("from outside 2")) // { value: "from fun: 2", done: false }
// "from outside 2"
console.log(g.next("from outside 3")) // { value: "from fun: 3", done: false }
// "from outside 3"
我们可以看到在yield语句返回的值就是next传进来的内容。同时要注意第一次调用next是传值没用的。从第二次开始调用next传参才用意义。
用generator实现async/await
相信看到这里,大家能发现generator跟async/await是有很多相似的地方的,如声明一个特殊的函数,在函数中可以用特殊的语句等。没错,async/await其实就是generator的语法糖。这时候也许会有大胆的朋友想问,我们能不能用generator实现async/await的功能呢?这其实也是一道面试题,我们今天就来一起尝试一下吧。
目标
我们先来确认一下,我们想要的效果是怎样的。假如现在有一个async/await代码
async function asyncFn() {
const num1 = await fn(1)
console.log(num1)
const num2 = await fn(num1)
console.log(num2)
const num3 = await fn(num2)
console.log(num3)
return num3
}
const asyncRes = asyncFn()
asyncRes.then(res => console.log(res))
那用generator写法应该希望是
function* gen() {
const num1 = yield fn(1)
console.log(num1)
const num2 = yield fn(num1)
console.log(num2)
const num3 = yield fn(num2)
console.log(num3)
return num3
}
const asyncRes = gen()
asyncRes.then(res => console.log(res))
但我们知道generator必须执行到底才会有具体的返回的,而async是执行后就会返回一个promise实例。所有我们肯定是要再套一层函数作处理的,一般这种函数叫高阶函数。
function* gen() {
const num1 = yield fn(1)
console.log(num1)
const num2 = yield fn(num1)
console.log(num2)
const num3 = yield fn(num2)
console.log(num3)
return num3
}
// genToAsync返回一个函数,作用就是执行过这个函数之后,gen就变成async函数了,是一个语义。
const genToAsync = generatorToAsync(gen)
// asyncRes得到一个promise实例
const asyncRes = genToAsync()
asyncRes.then(res => console.log(res))
generatorToAsync
经过上方提示,我们已经很明确generatorToAsync需要做到以下几点:
- 执行后应该是返回一个函数asyncFunc,这个函数就是表明,我们已经把generator转成async函数了的意思。
- asyncFunc执行后应该返回一个promise实例
- promise的状态根据asyncFunc函数中代码的执行有没有报错决定。同时如果有return值,promise的结果就是return的值。
function generatorToAsync(genFun){
// 返回asyncFunc函数
return function(){
// 先执行一次genFun
const gen = genFun.apply(this, arguments);
// 返回一个promise对象
return new Promise((resolve, reject) => {
// 定义一个go函数,作用是可以控制执行gen的不同函数,这里会用到next和throw
function go(key, arg) {
let res
try {
// 这里有可能会执行返回reject状态的Promise
res = gen[key](arg)
} catch (error) {
// 如果执行过程中报错,直接reject
return reject(error)
}
// gen执行完next之后的返回是一个对象,我们需要把done和value拿到。
const { value, done } = res
if (done) {
// 如果done为true,说明走完了,进行resolve(value)
return resolve(value)
} else {
// 如果done为false,说明没走完,还得继续走
// value有可能是:常量,Promise,Promise有可能是成功或者失败
// promise执行返回的值 通过next传给gen内部
return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
}
}
// 执行go
go("next")
})
}
}
底层概念
这里简单提一个v8实现async/await或者generator的机制,其中最重要的一个概念就是【协程】。协程是在线程下的一个运行机制,一个线程可以有多个协程,每个协程拥有自己的寄存器上下文和栈。但每次只能有一个协程运行。所有当代码运行到yield之后,gen函数中的协程就会把执行权交到外部。外部代码就会继续运行,当运行到next调用之后,协程又会交给gen函数内部。
总结
async/await是很常用的语法,但少人用会去了解他背后的原理。今天带大家再次了解了js在回调问题上的处理历史,并带大家回顾了生成器的使用。同时我们用生成器实现了async/await的功能。希望对大家有所帮助。
参考
developer.mozilla.org/zh-CN/docs/…