async/await最佳实践 🌕

随着async/await语法糖的发布,使用同步写法写异步代码一定会成为趋势。之前Promise使用的thencatch控制流程,
换用新的语法糖之后,要重新拾起try...catch了。

try…catch 基本用法

一个例子

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
function hello() {
try {
console.log(1);
throw new Error(2);
console.log(3);
} catch (e) {
console.log(4);
console.log("error message of function hello: " + e.message);
} finally {
console.log(5);
}
}

function world() {
try {
hello();
console.log(6);
} catch (e) {
console.log(7);
console.log("error message of function world: " + e.message);
} finally {
console.log(8);
}
}

world();

输出:

1
2
3
4
5
6
1
4
"error message of function hello: 2"
5
6
8

finally

代码执行后输出了58 说明了finally的功能,与Promise中的finally类似:无论上面的流程执行的如何,
finally中的代码都会执行。

catch

代码执行后输出了4error message of function hello: 2,说明了catch的功能,与Promise中的catch类似:
只要try里面的代码中抛出了异常,catch都能捕获到异常。

但是,为什么3没有输出呢?为什么6输出了?

Error

如果代码执行出现了错误,代码是不会继续向下执行的,所以3没有输出。
而如果错误被捕获了,并且处理了,代码就会继续执行,所以,6输出了。

但这里有一个问题,我们有时候不希望6被输出,因为之前代码已经出错了,6就不应该被输出。

try..catch 嵌套

上面的问题之所以出现,是因为我们使用了嵌套的try...catch,也与promisethen catch类似,嵌套在内部的函数(hello),如果发生异常,会被,内部函数的catch捕获,捕获后,并不会继续抛出让外部函数(world)继续捕获。如果需要,我们必须在内部函数的catch中,继续抛出这个错误。

修改代码如下:

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
function hello() {
try {
console.log(1);
throw new Error(2);
console.log(3);
} catch (e) {
console.log(4);
console.log("error message of function hello: " + e.message);
throw e;
} finally {
console.log(5);
}
}

function world() {
try {
hello();
console.log(6);
} catch (e) {
console.log(7);
console.log("error message of function world: " + e.message);
} finally {
console.log(8);
}
}

world();

输出:

1
2
3
4
5
6
7
1
4
"error message of function hello: 2"
5
7
"error message of function world: 2"
8

Bingo! 代码如我们希望中的样子执行了!异常被外部函数(world)捕获了,6没有被输出。
说完try...catch控制流程,我们看看它如何处理async/await的异步流程控制。

async/await

Promise 写法 VS async/await 写法

Promise 写法

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 hello() {
console.log(1);
return new Promise((resolve, reject) => {
setTimeout(() => reject(2), 0);
})
.then(res => {
console.log(3);
})
.catch(e => {
console.log(4);
console.log("error message of function hello: " + e);
return Promise.reject(e);
})
.finally(f => {
console.log(5);
});
}

function world() {
hello()
.then(res => {
console.log(6);
})
.catch(e => {
console.log(7);
console.log("error message of function world: " + e);
})
.finally(f => {
console.log(8);
});
}

world();

async/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
26
27
28
29
async function hello() {
try {
console.log(1);
await new Promise((resolve, reject) => {
setTimeout(() => reject(2), 0);
});
console.log(3);
} catch (e) {
console.log(4);
console.log("error message of function hello: " + e);
throw e;
} finally {
console.log(5);
}
}

async function world() {
try {
await hello();
console.log(6);
} catch (e) {
console.log(7);
console.log("error message of function world: " + e);
} finally {
console.log(8);
}
}

world();

看出来区别了吗?其实感觉没多大区别,除了代码少了 4 行,链式写法变成了同步写法。

但是,如果整个程序流程再复杂三倍呢?

我会义无反顾的选择async/await!

并行和串行

TODO

最佳实践

经过一段时间的使用,对于async/await的正确使用,我总结了一下几点:

  1. 必须使用 try...catch。确保正确的流程控制。

    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
    // resolve
    function getData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => resolve({ state: 1 }), 1000);
    });
    }
    // reject
    function getData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => reject({ error: 2 }), 1000);
    });
    }

    async function hello() {
    let data = await getData();
    console.log(data.state);
    }

    async function hello() {
    try {
    let data = await getData();
    console.log(data.state);
    } catch (e) {
    console.log(e);
    let data = {};
    }
    }

    hello();

    如果await后面的promise返回的是reject,那么下面的同步代码不会执行。所以必须使用try...catch,如果出现这种情况,
    会进入catch,执行错误处理代码。

  2. 多重嵌套时,内层的try...catch必须有返回值,且catch中的返回值是Promise.reject。返回值是promise

    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
    // resolve
    function getData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => resolve({ state: 1 }), 1000);
    });
    }
    // reject
    function getData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => reject({ error: 2 }), 1000);
    });
    }

    async function hello() {
    try {
    return await getData();
    } catch (e) {
    console.log(`${e} --- hello catch`);
    return Promise.reject(e);
    // return Promise.resolve(e)
    }
    }

    async function world() {
    try {
    // 不会执行
    data = await hello();
    } catch (e) {
    data = {};
    console.log(`${e} --- world catch`);
    }
    }
    world();

    world使用异步方式调用了hello,如果hello中的getData方法抛出了错误,会进入hello里的catch,则catch中也必须有返回值,否则,将不会进入world中的catch,而是继续执行try里面的代码;如果使用了Promise.resolve返回内容或者返回内容不是Promise,就也是执行try里面的代码;只用返回的结果是Promise且为Promise.reject,才会正常的进入world中的catch

  3. vue 的生命周期是独立的,不会受到异步函数的影响。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    export default {
    data() {
    return {
    data: null
    };
    },
    async created() {
    this.data = await this.getData();
    },
    mounted() {
    console.log(this.data);
    },
    methods: {
    getData() {
    return new Promise((resolve, reject) => {
    setTimeout(() => resolve({ state: 1 }), 1000);
    });
    }
    }
    };

    我们知道created函数会早于mounted被调用,但是mounted并不会等待created中的异步函数,vue的生命周期只跟其内部的执行流程有关,不受外界的影响。

总结

就如我一开始所说,async/await并不是什么新的技术,而只是语法糖。
但是,如果要替代现在的promise,特别是在多人团队中推广,我还是建议先去学习一下语法,尤其是异常处理和多层嵌套下向外抛出异常,不然,….说多了都是坑 😂。
这篇文章只是讲了async/await的具体实践,如果你想了解promiseasync/await的实现原理,请移步Promise实现原理 🌑 从Promise到async/await 🌑