Fork me on GitHub

async函数理解与实现

async函数是什么?

我们创建了 promise 但不能同步等待它执行完成。我们只能通过 then 传一个回调函数这样很容易再次陷入 promise 的回调地狱。实际上,async/await 在底层转换成了 promise 和 then 回调函数。

每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。

async函数作用是什么?

我们创建了 promise 但不能同步等待它执行完成。我们只能通过 then 传一个回调函数这样很容易再次陷入 promise 的回调地狱。async函数优化了promise 的回调问题,被称作是异步的终极解决方案。

async函数内部做了什么?

async 函数会返回一个 Promise 对象,如果在函数中 return 一个直接量(普通变量),async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。如果返回了promise那就以返回的promise为准。

如果async函数没有返回值呢?它会返回 Promise.resolve(undefined)。

1
2
3
4
5
async function fn () {
return 1;
}

fn().then(alert) // 1

也可以显式的返回一个promise,这个将会是同样的结果:

1
2
3
4
5
async function fn () {
return Promise.resolve(1);
}

fn().then(alert) // 1

async确保了函数返回一个promise,即使其中包含非promise。

await关键字?

按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值。

虽然await后面通常是一个异步操作(promise),但是这不代表 await 后面只能跟异步操作,也就是说await后面实际是可以接普通函数调用或者直接量的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getSomething() {
return "something";
}

async function testAsync() {
return Promise.resolve("hello async");
}

async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}

test(); // something hello async

关键词await可以让JavaScript进行等待,直到一个promise执行并返回它的结果,JavaScript才会继续往下执行。

1
2
3
4
5
6
7
8
9
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done!'), 1000)
});

async function test() {
let result = await promise;
alert(result);
}
test(); // 'done'

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?其实await是个运算符,用于组成表达式,awai 表达式的运算结果取决于它等的东西。

  • 如果 await 后面跟的不是一个 Promise对象,那 await 后面表达式的运算结果就是它等到的结果;
  • 如果 await 后面跟的是一个 Promise 对象,await 它会“阻塞”后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值作为 await 表达式的运算结果。

⚠️ async 函数调用不会造成“阻塞”,它内部所有的“阻塞”都被封装在一个 Promise对象中异步执行。因此这里的阻塞理解成异步等待更合理。这也就是 await必须用在async函数中的原因。

async/await如何使用?

async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

例如,用 setTimeout 模拟耗时的异步操作,先来看看不用async/await的写法:

1
2
3
4
5
6
7
8
9
function takeLongTime () {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

takeLongTime().then(v => {
console.log("got", v);
});

改用async/await呢?

1
2
3
4
5
6
7
8
9
10
11
12
function takeLongTime () {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

async function test () {
let res = await takeLongTime();
console.log(res);
}

test();

async/await 的优势?

单一的 Promise 链并不能发现 async/await 的优势,但是如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/

function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}

function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}

function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}

function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}

promise来实现三个步骤的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function doIt () {
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(res => {
console.log(res);
})
}
doIt();
/*
step1 with 300
step2 with 500
step3 with 700
900
*/

用async/await来写,结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样:

1
2
3
4
5
6
7
8
9
async function doIt () {
const time1 = 300;
let time2 = await step1(time1);
let time3 = await step2(time2);
let res = await step3(time3);
console.log(res);
}

doIt();

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}

function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}

function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}

用async/await来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function doIt() {
const time1 = 300;
let time2 = await step1(time1);
let time3 = await step2(time1 , time2);
let res = await step3(time1, time2, time3);
console.log(res);
}

doIt();

/*
step1 with 300
step2 with 300 and 500
step3 with 300, 500 and 1000
*/

promise的方式实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function doIt() {
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(res => {
console.log(res);
})
}
doIt();

错误处理

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

写法1:

1
2
3
4
5
6
7
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}

写法2:

1
2
3
4
5
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

1
2
3
4
5
6
7
8
async function dbFuc(db) {
let docs = [{}, {}, {}];

// 报错 Uncaught SyntaxError: await is only valid in async function
docs.forEach(function (doc) {
await db.post(doc);
});
}

但是,如果将 forEach 方法的参数改成 async 函数,也有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function fetch(x) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(x)
}, 500 * x)
})
}

function test() {
let arr = [3, 2, 1]
arr.forEach(async item => {
const res = await fetch(item)
console.log(res)
})
console.log('end')
}

test();

期望的输出结果应该是3 2 1 end,然而实际打印的结果是:end 1 2 3,为什么?

forEach 只支持同步代码。可以参考下 Polyfill 版本的 forEach,简化以后类似就是这样的伪代码:

1
2
3
4
while (index < arr.length) {
// 也就是我们传入的回调函数
callback(item, index)
}

从上述代码中我们可以发现,forEach 只是简单的执行了下回调函数而已,并不会去处理异步的情况。并且你在 callback 中即使使用 break 也并不能结束遍历。

正确的写法是采用 for…of。

1
2
3
4
5
6
7
8
async function test() {
let arr = [3, 2, 1]
for (const item of arr) {
const res = await fetch(item)
console.log(res)
}
console.log('end')
}

因为 for…of 内部处理的机制和 forEach 不同,forEach 是直接调用回调函数,for…of 是通过迭代器的方式去遍历。

1
2
3
4
5
6
7
8
9
10
11
12
async function test() {
let arr = [3, 2, 1]
const iterator = arr[Symbol.iterator]()
let res = iterator.next()
while (!res.done) {
const value = res.value
const res1 = await fetch(value)
console.log(res1)
res = iterator.next()
}
console.log('end')
}

以上代码等价于 for…of,可以看成 for…of 是以上代码的语法糖。

如果确实希望多个请求并发执行,可以使用 Promise.all 方法。

1
2
3
4
5
6
7
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = await Promise.all(promises);
console.log(results);
}

async实现原理

async 就等于Generator+自动执行器。

Generator

1
2
3
4
5
6
7
8
9
10
var x = 1;

function *foo () {
x++;
yield 'hello';
x++;
console.log(x);
}

var it = foo();

生成器*foo()没有像普通函数一样运行,它只是创建了一个迭代器。

1
2
3
4
it.next()
// {value: "hello", done: false}
it.next()
// 3 {value: undefined, done: true}
  • 第一次调用next(),会从函数开始位置执行到第一个暂停点(yield处),它返回了一个对象,对象的value值就是当前yield的值。此时执行了一次x++;所以x的值为2。
  • 再次执行next()会运行到函数结束。

这种方式可以让函数体的代码分段执行,需要手动next()来控制代码的进度。所以用它来处理异步的方式也比较明了。yield暂停函数体代码,当异步操作完成后再使用next()恢复函数的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function readFile (a) {
return new Promise(resolve => {
setTimeout(()=>{
console.log(a);
resolve(a);
}, 500);
});
}

function *foo () {
var res = yield readFile('a');
console.log('b');
}

var it = foo();
var result = it.next(); //next返回的value是readFile函数返回的Promise对象
result.value.then(()=>{ //给Promise对象增加成功的回调
it.next(); //当Promise成功后恢复foo()函数执行
}); //a b

执行到readFile的时候暂停了foo函数,直到Promise被解决后调用了next(),才恢复了函数。

自动执行器

可以看到,it.next()返回{ value:x,done:false }。根据done的值是可以知道生成器内部代码是否执行完成。所以写个递归就可以了,注意异步操作返回结果才可以继续执行

1
2
3
4
5
6
7
8
function run(g) {
var res = g.next(); // res.value是个promise对象
if(!res.done) {
res.value.then(() => { //promise解决了才调用next()继续执行生成器内部函数
run(g);
});
}
}

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function readFile(a){
return new Promise(resolve=>{
setTimeout(()=>{
console.log(a);
resolve(a);
},500)
})
}

function *foo(){
console.log('a');
var result = yield readFile('b');
console.log('c');
}

function run(g){
var res = g.next(); //记住res.value是个promise对象
if(!res.done){
res.value.then(()=>{ //promise解决了才继续执行生成器内部函数
run(g);
})
}
}

run(foo());
console.log('d');

// a d b c
  • 第一次执行foo().next()打印出a, 然后生成器内函数暂停, 打印d
  • 定时器到了后再打出b和c。这个简单的自动执行器,是针对yield后面跟着promise对象的情况。实际使用可能不能这么写,它只是帮助你理解。实战可以选择co模块。

async 等于Generator+自动执行器

1
2
3
4
async function test(){};
test();
//等价于
run((function *test(){})());

模拟实现async/await总结

将 Generator 函数和自动执行器,包装在一个函数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function spawn(genF) {
return new Promise(funciton(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch (e) {
return reject(e);
}

if (next.done) {
return resolve(next.value)
}

Promise.resolve(next.value).then(
function (v) {
step(function() {
return gen.next(v);
});
},
function (e) {
step(function() {
return gen.throw(e);
});
}
)
}
step(function() {
return gen.next(undefined);
});
})
}

总结

放在一个函数前的async有两个作用:

  1. 使函数总是返回一个promise
  2. 允许在这其中使用await

promise前面的await关键字能够使JavaScript等待,直到promise处理结束。然后:

  1. 如果它是一个错误,异常就产生了,就像在那个地方调用了throw error一样。
  2. 否则,它会返回一个结果,我们可以将它分配给一个值

async/await函数实现原理:将 Generator 函数和自动执行器,包装在一个函数里

参考文章:
http://www.ruanyifeng.com/blog/2015/05/async.html
https://juejin.im/post/5a9516885188257a6b061d72
https://javascript.info/async-await
https://segmentfault.com/a/1190000007535316?utm_source=tag-newest
https://juejin.im/post/5cb1d5a3f265da03587bed99?utm_source=gold_browser_extension

本文标题:async函数理解与实现

文章作者:tongtong

发布时间:2019年04月12日 - 16:04

最后更新:2019年04月17日 - 15:04

原始链接:https://ilove-coding.github.io/2019/04/12/async函数理解与实现/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------