ES6 async函数(一)

注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:

阮一峰ES6的书籍中ES6 async函数
我的ES6入门学习规划

定义

async函数是Generator函数的语法糖。使用async函数等同于使用Generator函数加co模块。

这是使用自己封装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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function * executeAsyncTasks() {
// 核心就是把异步任务从timer换成Promise

let resultForTask1 = yield new Promise(function(resolve){
setTimeout(function(){
console.log('first async task executed');
resolve({name: 'task1', state: 'executed', next: Math.random() * 10 > 5 ? 'second' : 'third'});
},1000);
});

let resultForNext;
if(resultForTask1.next === 'second') {
resultForNext = yield new Promise(function(resolve){
setTimeout(function(){
console.log('second async task executed');
resolve({name: 'task2', state: 'executed'});
},1000);
});
} else {
resultForNext = yield new Promise(function(resolve){
setTimeout(function(){
console.log('third async task executed');
resolve({name: 'task3', state: 'executed'});
},1000);
});
}

return [resultForTask1, resultForNext];
}

function autoRunGenerator(gen, callback) {
return new Promise(function(resolve, reject){
function run(lastResult){
let ret = gen.next(lastResult);

if(!ret.done) {
ret.value.then(function(result) {
run(result);
}).catch(function(error) {
reject(error);
})
} else {
resolve(ret.value);
}
}

run();
});
}

autoRunGenerator(executeAsyncTasks()).then(function(ret) {
console.log(ret);
});

用async函数可以改写成:

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
async function executeAsyncTasks() {
// 核心就是把异步任务从timer换成Promise

let resultForTask1 = await new Promise(function(resolve){
setTimeout(function(){
console.log('first async task executed');
resolve({name: 'task1', state: 'executed', next: Math.random() * 10 > 5 ? 'second' : 'third'});
},1000);
});

let resultForNext;
if(resultForTask1.next === 'second') {
resultForNext = await new Promise(function(resolve){
setTimeout(function(){
console.log('second async task executed');
resolve({name: 'task2', state: 'executed'});
},1000);
});
} else {
resultForNext = await new Promise(function(resolve){
setTimeout(function(){
console.log('third async task executed');
resolve({name: 'task3', state: 'executed'});
},1000);
});
}

return [resultForTask1, resultForNext];
}

executeAsyncTasks().then(function(ret) {
console.log(ret);
});

对比下来发现,async函数有如下特点:

  1. async函数不需要执行器,一旦调用,函数体自动执行;
  2. 函数没有了*号,但是需要用到async关键词
  3. 内部不再使用yield表达式,而是使用await表达式

async函数调用后,内部函数体自动开始运行,同时返回一个Promise对象。内部函数体可能包含任意个await表达式,分别代表一个异步任务,每一个异步任务的结果,会作为整个await表达式的值,赋值或传递给其它变量;await的异步任务,默认是继发关系,必须前面的结束,后面的才会开始执行,也就是说虽然await后面的任务是异步的,但是整个async函数体还是同步的;等到所有代码都执行完以后,内部函数体的返回值会用来fulfill掉async函数调用后返回的那个Promise。

async函数解决了前面学习Generator函数异步应用时的所有问题,而且用起来更加简洁、更加语义化,所以当我们选择要用Generator函数来编写异步任务的时候,async函数是首选方式。

更多用法

  • async函数的返回值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function test(){
    await new Promise(resolve=>{
    setTimeout(()=>{
    resolve();
    },1000);
    })

    return 'end';
    }

    test().then(state=>{
    console.log(state);
    })

async函数返回一个Promise,在整个函数体运行完以后,它会被fulfilled,通过then注册回调可以拿到函数体最后返回的值。

async里面可以没有await关键词,就像Generator函数可以没有yield关键词一样。

1
2
3
4
5
6
7
async function test(){
return 'end';
}

test().then(state=>{
console.log(state);
})

await后面可以接另外一个async函数调用,因为async返回的是Promise,代表的就是一个异步任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function test(){
await new Promise(resolve=>{
setTimeout(()=>{
resolve();
},1000);
})

return 'end';
}

async function foo(){
await test();
return 'foo end';
}

foo().then(state=>{
console.log(state);
})

async可以用于很多地方,比如函数声明,函数表达式以及对象或类的定义里面:

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
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

  • await其实可以接任意数据,大部分情况下是Promise
    await后面接的数据,在ES6内部,应该会通过Promise.resolve方法转一下,所以await后面才能接任意数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function foo() {
    let ret = await {name: 'lyzg'};

    return ret.name;
    }

    foo().then(name=>{
    console.log(name);
    });

错误处理

async函数返回的Promise,在下面几种情况下都会被reject:

  1. 函数体内执行时出错,抛出异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function foo() {
    let ret = await 1;

    throw new Error('test');

    return 2;
    }

    foo().catch(e=>{
    console.error(e);
    });
  2. 函数体最后返回一个rejected的Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function foo() {
    let ret = await 1;

    return Promise.reject(new Error('rejected return'));
    }

    foo().catch(e=>{
    console.error(e);
    });
  3. 某个await后面的异步任务被rejected,并且没有通过catch进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function foo() {
    let ret = await new Promise((resolve, reject)=>{
    reject('await rejected');
    });

    console.log('later logic will not execute');

    return 2;
    }

    foo().catch(e=>{
    console.error(e);
    });

只要一个await发生reject,函数体后续逻辑都不会再运行,且async函数返回的Promise也会被reject。

如果await后面的异步任务,最后加了catch处理,那么就不会出现上面的情况。其实很简单,因为一个Promise链中,catch方法返回的是一个新的Promise,它是被fulfilled还是被reject,取决于catch回调内的逻辑,Promise链最后一个Promise不被reject,那么await就不会失败。

另外一个防止await出错时,导致后续任务不能继续的情况,是把await放置在try-catch块中进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);

console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}

为啥加try-catch可以这么做,等后面掌握了async函数的实现原理就明白了。

实现原理

async函数说白了还是在自动执行Generator函数,它的执行机制,跟前面的笔记中用Promise实现Generator函数异步应用是差不多的,核心还是那个Generator函数的自动执行器。这是async的执行器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function autoRunGenerator(gen) {
return new Promise(function(resolve, reject) {
function run(nextF) {
let ret;
try {
ret = nextF();
} catch(e) {
return reject(e);
}
if(ret.done) {
return resolve(ret.value);
}
Promise.resolve(ret.value).then(function(v) {
run(function() { return gen.next(v); });
}, function(e) {
run(function() { return gen.throw(e); });
});
}
run(function() { return gen.next(undefined); });
});
}

之前笔记中Promise的实现版本是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function autoRunGenerator(gen, callback) {
return new Promise(function(resolve, reject){
function run(lastResult){
let ret = gen.next(lastResult);

if(!ret.done) {
ret.value.then(function(result) {
run(result);
}).catch(function(error) {
reject(error);
})
} else {
resolve(ret.value);
}
}

run();
});
}

async的执行器主要有3点改进:

  1. try-catch包裹了next方法调用,这样Generator函数体出错,都能反映到这个执行器返回的那个Promise身上

    1
    2
    3
    4
    5
    try {
    ret = nextF();
    } catch(e) {
    return reject(e);
    }
  2. 内部run方法的参数,不是一个固定的lastResult参数,而是一个回调参数,因为如果用lastResult参数,相当于默认了,下一次run调用的内部一定是调用gen.next,但实际上可能不是,还可能是throw:

    1
    2
    3
    4
    5
    Promise.resolve(ret.value).then(function(v) {
    run(function() { return gen.next(v); });
    }, function(e) {
    run(function() { return gen.throw(e); });
    });

    这个throw方法也很妙。 这就是为什么async函数体内,能够用try-catch捕获await后面的Promise被rejected时错误的关键点。

  3. 通过nextF,拿到yield后面的表达式值后,不是直接就做then调用添加回调,而是先调用Promise.resolve,这就是为啥await后面能接任意数据的关键点:

    1
    Promise.resolve(ret.value).then(function(v) {

使用async版本的执行器,等同于使用async函数:

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
34
35
36
37
38
39
function autoRunGenerator(gen) {
return new Promise(function(resolve, reject) {
function run(nextF) {
let ret;
try {
ret = nextF();
} catch(e) {
return reject(e);
}
if(ret.done) {
return resolve(ret.value);
}
Promise.resolve(ret.value).then(function(v) {
run(function() { return gen.next(v); });
}, function(e) {
run(function() { return gen.throw(e); });
});
}
run(function() { return gen.next(undefined); });
});
}

function * asyncTask() {
let ret = yield new Promise(resolve=>{
setTimeout(()=>{
resolve({task: 'task1'});
}, 1000);
});

console.log(ret);

return 'finished';
}

autoRunGenerator(asyncTask()).then(state=>{
console.log(state);
});
// {task: "task1"}
// finished

继发与并发

await后面的任务是继发的,先执行完前面的,后面的异步任务才能执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function foo() {
console.log('task1 started');
let task1 = await new Promise(resolve=>{
setTimeout(()=>{
resolve(true);
console.log('task1 finished');
});
});
console.log("task2 started");
let task2 = await new Promise(resolve=>{
setTimeout(()=>{
resolve(true);
console.log('task2 finished');
});
});
}

foo();
// task1 started
// task1 finished
// task2 started
// task2 finished

但有时并不想这么干,更希望异步任务同时开始执行,这时只需要把异步任务写在await前面,而不是直接写到await后面,即可:

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
async function foo() {
console.log('task1 started');
let task1 = new Promise(resolve=>{
setTimeout(()=>{
resolve(true);
console.log('task1 finished');
});
});
console.log("task2 started");
let task2 = new Promise(resolve=>{
setTimeout(()=>{
resolve(true);
console.log('task2 finished');
});
});

await task1;
await task2;
}

foo();
// task1 started
// task2 started
// task1 finished
// task2 finished