写在开头
一句话总结:async/await就是Generator函数的语法糖。
什么是async/await
async/await是针对js异步问题的一种处理方法。
如果将关键字async放在函数声明之前,则表明该函数是一个异步函数。该异步函数会返回一个Promise对象,当函数执行完毕的时候,我们可以像使用Promise一样进行调用then方法处理后续流程。
await关键字需要和async一起使用。如果我们将await关键字放在异步函数调用之前,则当代码执行到此处的时候,代码将会暂停,等到异步函数执行完成后,才会继续向下执行。
我们可以先来看一个demo。
DEMO
function promise1() {
return new Promise((resolve, reject) => {
console.log('promise1 start');
setTimeout(() => {
resolve('promise1 callback');
}, 3000)
})
}
function promise2() {
return new Promise((resolve, reject) => {
console.log('promise2 start');
setTimeout(() => {
return resolve('promise2 callback');
}, 2000)
})
}
async function asyncDemo() {
console.log('function start');
let res1 = await promise1();
console.log('promise1 end', res1);
let res2 = await promise2();
console.log('promise2 end', res2);
console.log('function end);
return 'finish';
}
asyncDemo().then(res => {
console.log(res)
});
从控制台我们可以看到整个函数是以同步的方式向下执行的,在promise1和promise2返回值之前,下面的部分都被阻塞, 等到异步函数执行结束的时候才会继续向下执行。
async/await 原理思考
通过上面的demo, 我们可以发现整个async/await有两个特点
1、 返回的是一个Promise对象;
2、 await会阻塞代码的执行,在异步执行完成后,继续向下进行;
关于第一条,比较好实现,我们将整个函数包裹在一个Promise对象内返回,即可保证返回的是一个Promise对象。主要是第二点,我们需要在await的位置阻塞程序的执行,并且如果await 后面跟着的是一个异步函数的话, 我们需要在等待异步函数执行完毕,再继续执行后续代码。 在这里,我们可以利用Generator函数。
什么是Generator函数
Generator函数其实也是ES6提供的一种异步编程解决方案。
在阮一峰老师的Generator 函数的语法是这样描述的
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
在我个人看来,Generator其实是提供了一种方式,允许我们按照我们想要的方式去暂停/执行函数里面的代码, 类似于debug产生的断点,当然也不尽相同。
Generator函数有两个特征
1、 function关键字和函数名之间有一个*;
2、 内部使用yield进行状态控制。
3、 使用next执行下一步操作直到遇到yield关键字或者return
我们举例简单描述一下
function* generatorDemo() {
let value1 = yield 'hello';
let value2 = yield 'world';
console.log(value1 + ' ' + value2);
}
let demo = generatorDemo();
console.log(demo.next()) // {value: 'hello', done: false}
console.log(demo.next()) // {value: 'world', done: false}
console.log(demo.next()) // {value: undefined, done: true}
上面的例子该怎么解释呢? 我们调用generatorDemo()并不是执行了该函数,而是返回了一个指向内部状态的指针对象(我们这里赋值给demo)。后面需要使用next方法来使指针指向下一个状态。
每一次next方法会返回一个对象,
{
value: // yield后跟表达式的返回值,
done: // 指针是否指向了函数末尾 即函数是否执行完毕
},
value代表yield后面跟着的表达式返回的值,如果一直没有遇到yield则一直往下执行,如果遇到return,则将return后跟的表达式的值作为value的值。 如果函数没有return, 则value的值为undefined。
done表示指针是否遍历完所有的yield对象,遇到return或者函数结束。
这里有一点值得注意的是,如果你运行上面这段代码,console.log(value1 + '' + value2)打印出来的值是undefined undefined。这是因为yield本身没有返回值,或者说返回值就是undefined。 如果我们需要上一步的yield的返回值,我们需要这样做
function* generatorDemo() {
let value1 = yield 'hello';
let value2 = yield 'world';
console.log(value1 + ' ' + value2);
}
let demo = generatorDemo();
let res1 = demo.next();
let res2 = demo.next(res1.value);
demo.next(res2.value);
// 执行结果
// hello world
这是因为next方法可以接受一个参数, 这个参数会被当作上一步yield的返回值,代入到接下来的运算当中。
在next方法传入任何数据都是可以的哈
你可能要问了,Generator函数需要我们手动执行下一步操作,而async/await是调用之后就可以自行往下执行的, 我们要怎么让Generator函数自动执行呢? 别急, 我们接下来会讲到。
为了让Generator函数自动执行,我们需要用到Thunk函数的思想。
Thunk函数
在JavaScript中Thunk函数是指将一个多参数函数替换成一个只接受回调函数作为参数的单参数函数。
举个例子
// 原函数
readFile(fileName, callback);
// thunk函数
function thunk(fileName) {
return function(callback) {
return readFile(fileName, callback);
}
}
let demo = thunk('a.text');
demo(callback);
原本readFile需要接收两个参数,fileName文件名、callback回调函数。 我们通过thunk函数进行包装,只需要接收回调函数callback即可。 当然,这个例子看起来是有点画蛇添足的感觉, 但是这里只是仅仅说明thunk的思想, 我们接下来要用thunk的思想来包装generator函数,使它可以自执行。
// Generator函数
function* generatorDemo() {
let value1 = yield 'hello';
let value2 = yield 'world';
console.log(value1 + ' ' + value2);
}
// thunk包装
function geneThunk(fn) {
const root = fn();
function next(data) {
let result = root.next(data);
if (result.done) {
return result.value;
}
next(result.value);
}
next(undefined);
}
geneThunk(generatorDemo); // hello world
这里我们在genThunk内部定义了一个next函数。通过每次返回值的done属性确定函数是否执行完毕,执行完则将获得的value值作为返回值返回。 如果没有,则将获取的value作为上一步执行的结果传入next函数,继续执行next。
看到这里我想你应该对Generator函数和Thunk函数有一个简单的了解。
(有点乱立flag的感觉,如果不懂的话可以看下这个哈) 阮一峰老师关于tnhunk的描述
使用Generator函数实现async/await
接下来我们尝试去实现一个async/await。
这是我们之前的demo
function promise1() {
return new Promise((resolve, reject) => {
console.log('promise1 start');
setTimeout(() => {
resolve('promise1 callback');
}, 3000)
})
}
function promise2() {
return new Promise((resolve, reject) => {
console.log('promise2 start');
setTimeout(() => {
return resolve('promise2 callback');
}, 2000)
})
}
async function asyncDemo() {
console.log('function start');
let res1 = await promise1();
console.log('promise1 end', res1);
let res2 = await promise2();
console.log('promise2 end', res2);
console.log('function end);
return 'finish';
}
asyncDemo().then(res => {
console.log(res)
});
我们先将asyncDemo改成Generator函数
function* asyncDemoGen() {
console.log('function start');
let res1 = yield promise1();
console.log('promise1 end', res1);
let res2 = yield promise2();
console.log('promise2 end', res2);
console.log('function end);
return 'finish';
}
如我们在开头所说的一样,async/await其实是Generator函数的语法糖, 现在我们来实现Thunk函数
以async开头的函数 要返回一个Promise对象。
function asyncDemoThunk(fn) {
return new Promise(function(resolve, reject) {
const root = fn();
function next(data) {
let result = root.next(data);
if (result.done) {
return resolve(result.value);
}
next(result.value);
}
next(undefined);
})
}
因为await后面很可能跟着的是一个异步函数(肯定是咯),因此我们要在异步函数回调之后再往下执行。 因此我们对next做一下修改
function asyncDemoThunk(fn) {
return new Promise(function(resolve, reject) {
const root = fn();
function next(data) {
...
Promise.resolve(result.value).then(res => {
next(res);
});
}
next(undefined);
})
}
完整代码如下
function asyncDemoThunk(fn) {
return new Promise(function(resolve, reject) {
const root = fn();
function next(data) {
let result = root.next(data);
if (result.done) {
return resolve(result.value);
}
Promise.resolve(result.value).then(res => {
next(res);
})
}
next(undefined);
})
}
总结
其实,正如开头所说async/await是Generator的语法糖, async会将一个函数转化为generator函数,函数内部的await被替换为yield。 这里也是简单讲解了一下原理,目的是希望自己能够加深一下对async/await的理解,也分享出来能够给大家一些启发。 如果有不足之处,也希望各位大佬能够斧正。
参考
1、 generator函数
3、async函数
建议各位先按照链接顺序看一遍,在读完3的时候,可以结合1、2自己实现一下,印象会更深刻哈。