写在开头
一句话总结: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自己实现一下,印象会更深刻哈。
复制代码