js 中异步编程实践总结
作者: dkvirus 发表于: 2017-09-25 15:01:00 最近更新: 2018-08-01 22:08:16

本文总结 js 中异步编程的两种方式:Promise 和 async/await。

一、JS 异步编程之 Promise

1. 传统异步解决方案

在 Promise 等 es6 异步编程出现之前,传统异步是通过回调函数进行处理的。想一想 ajax 的 success 函数就是处理 ajax 异步成功的一个回调函数。

1.1 什么是异步

传统编程都是顺序执行代码的,下面代码正常输出顺序应该是:step1 > step2 > step3。

1
2
3
4
5
6
7
8
9
10
// asyncfunc.js
function step2 () {
setTimeout (() => {
console.log('step2');
}, 3000);
}

console.log('step1');
step2();
console.log('step3');

在控制台打印结果:先打印 step1 和 step3,过几秒之后才会打印出 step2,这无疑与传统编程的顺序执行并不同。这种变更代码执行顺序的行为就叫做异步。

1
2
3
4
D:\code\es6\promise-demo>node asyncfunc.js
step1
step3
step2

常见的异步操作有 ajax,这里的 setTimeout 是模拟异步操作的一种方式。

1.2 回调函数完成异步操作

在出现 es6 异步编程之前,传统的异步是通过回调函数完成的。下面实现一个泡茶操作:先烧水(水烧开需要5秒),再进行泡茶。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// callbackForAsync.js   
// v1.0

// 烧水
function boilWater () {
setTimeout(() => {
console.log('水刚刚烧开,可以泡茶了');
}, 5000);
}

// 泡茶
function makeTea () {
console.log('水已经烧开了,开始泡茶');
}

boilWater(); // 烧水
makeTea(); // 泡茶

这里写了两个方法,一个烧水,一个泡茶,烧水方法里用了一个延迟函数 setTimeout,因为烧水这个过程需要 5 s 时间。之后调用这两个方法,先调用烧水,再调用泡茶。

1
2
3
D:\code\es6\promise-demo>node callbackForAsync.js
水已经烧开了,开始泡茶
水刚刚烧开,可以泡茶了

尽管我们先调用了烧水方法,再调用泡茶方法,但打印的结果与我们期待的并不同。这是因为这个过程用到了异步的思想。烧水是个异步过程,但是这里的写法是顺序执行的,泡茶并不会等待烧水完成再执行,而我们期望的就是泡茶等待烧水完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// callbackForAsync.js 
// v2.0

// 烧水
function boilWater (callback) {
setTimeout(() => {
console.log('水刚刚烧开,可以泡茶了');
callback();
}, 5000);
}

// 泡茶
function makeTea () {
console.log('水已经烧开了,开始泡茶');
}

boilWater(makeTea); // 烧水 + 泡茶

对 v1.0 代码进行修改,将泡茶方法作为参数传递给烧水方法,在烧水方法内部调用调用方法就可以实现我们的期待值。

1
2
3
D:\code\es6\promise-demo>node callbackForAsync.js
水刚刚烧开,可以泡茶了
水已经烧开了,开始泡茶

1.3 回调地狱

前面只有两步操作,通过一个回调可以很容易的实现,现在加一个操作,泡完茶之后进行喝茶操作,如何实现??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// callbackHell.js

// 烧水
function boilWater (callback, callback2) {
setTimeout(() => {
console.log('水刚刚烧开,可以泡茶了');
callback(callback2);
}, 5000);
}

// 泡茶
function makeTea (callback2) {
console.log('水已经烧开了,开始泡茶');
callback2();
}

// 喝茶
function drinkTea () {
console.log('茶泡好了,正在喝茶');
}

// 烧水 > 泡茶 > 喝茶
boilWater(makeTea, drinkTea);

上述代码中,将喝茶操作作为参数先传递给烧水方法,在烧水方法内部将喝茶方法作为参数传递给泡茶方法,最后在泡茶方法内部再调用喝茶方法。可见这里只有两步逻辑,喝茶方法在烧水方法和泡茶方法都有出现,如果喝茶操作之后还有操作,那么类推会不停的进行嵌套嵌套,这种实现不仅不美观,而且还不方便后期代码维护。

2. Promise 是什么?

Promise1.png

在谷歌浏览器的控制台(按 F12)中打印 console.dir(Promise),可以看到上图。

  • Promise 是一个构造函数(只有构造函数的函数名首字母才大写,这是规范),本身拥有三个方法:all、race 、reject、resolve;
  • 原型链对象拥有两个方法:catch、then,原型链上的方法通过 new 实例对象才能调用。

3. Promise 基础写法

3.1 创建 Promise 实例对象

1
2
3
4
5
6
// promiseBaseDemo.js
// v1.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 内部代码');
resolve('end');
});

Promise 是个构造函数,通过 new 得到实例对象 myPromise;构造函数的参数有两个 resolve 和 reject 两个形参,这两个参数就是在控制台中输入 console.dir(Promise) 打印出来的那两个属于构造函数的方法,有什么用,接下来说。

1
2
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 内部代码

结果打印出了实例对象内部的代码。通常来说使用 new 创建的实例对象并不会打印任何信息,只有调用这个方法,如:myPromise() 才会执行代码,但是这里却打印了东东。

特性:Promise 构造出的实例对象会自执行。

3.2 实例对象的 then 方法

1
2
3
4
5
6
7
8
9
10
// promiseBaseDemo.js
// v2.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 内部代码');
resolve('end');
});

myPromise.then(function (data) {
console.log(data);
});

myPromise 是 Promise 的实例对象,拥有原型链方法 then。

1
2
3
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 内部代码
end

then 方法为 Promise 的原型链方法,接收一个函数作为参数,如上述代码,data 表示 new 实例对象时 resolve() 里面的内容。

3.3 实例对象的 catch 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// promiseBaseDemo.js
// v3.0
var myPromise = new Promise(function (resolve, reject) {
console.log('peomise 内部代码');

if (0 > 1) {
resolve('end');
} else {
reject('出错了');
}
});

myPromise.then(function (data) {
console.log(data);
}).catch(function (error) {
console.log(error);
});

resolve 返回成功的数据,reject 返回失败的数据。楼主刚开始学习这里不是很理解,这里写贴上代码看下打印结果,下面通过实例感受区别。
catch 方法用来处理异常,也就是处理 reject 方法,保持程序不会直接挂掉,仍然可以继续执行。同样的 then 方法就是处理 resolve 方法。

1
2
3
D:\code\es6\promise-demo>node promiseBaseDemo.js
peomise 内部代码
出错了

3.4 规避 Promise 对象自执行

前面说到了 Promise 创建实例对象会自执行,这显示不是我们想要的,作为控制欲强盛的程序员,要做到我想让你执行你才能执行,不想让你执行就不能执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// promiseBaseDemo.js
// v4.0
function myPromise () {
return new Promise(function (resolve, reject) {
console.log('peomise 内部代码');

if (0 > 1) {
resolve('end');
} else {
reject('出错了');
}
});
}

// 想要执行解除下面代码的注释
// myPromise().then(function (data) {
// console.log(data);
// }).catch(function (error) {
// console.log(error);
// });

将 new Promise 这个过程封装到一个函数中,并且在函数内部返回 Promise 实例对象。

注意:执行方法变成了 myPromise() 而不是之前的 myPromise。

4. Promise 同步控制异步操作执行顺序

4.1 多个异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// multiAsync.js

var boilWater = function () {
setTimeout(() => {
console.log('step1: 水刚刚烧开,可以泡茶了');
}, 5000);
}

var makeTea = function () {
setTimeout(() => {
console.log('step2: 水已经烧开了,开始泡茶');
}, 2000);
}

// 喝茶:异步操作,需要 1 s
var drinkTea = function () {
setTimeout(() => {
console.log('step3: 茶泡好了,正在喝茶');
}, 1000);
}

boilWater();
makeTea();
drinkTea();
1
2
3
4
D:\code\es6\promise-demo>node multiAsync.js
step3: 茶泡好了,正在喝茶
step2: 水已经烧开了,开始泡茶
step1: 水刚刚烧开,可以泡茶了

像上面的代码,多个异步操作,按照正常写法,我们无法控制多个异步操作的执行顺序。Promise 可以控制多个异步操作的顺序,并且告别回调地狱,按照同步的写法去书写。

4.2 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
34
35
36
37
38
39
40
41
42
43
44
// promiseForAsync.js

// 烧水:异步操作,需要 5 s
var boilWater = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step1: 水刚刚烧开,可以泡茶了');
}, 5000);
});
}

// 泡茶:异步操作,需要 2 s
var makeTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step2: 水已经烧开了,开始泡茶');
}, 2000);
});
}

// 喝茶:异步操作,需要 1 s
var drinkTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step3: 茶泡好了,正在喝茶');
}, 1000);
});
}

var arr = []; // 创建数组,记录三个异步操作执行顺序

console.time('promise');
boilWater().then((data) => {
arr.push(data);
// makeTea() 返回的是 Promise 的实例对象,依次可以继续使用 then 方法。下同。
return makeTea();
}).then((data) => {
arr.push(data);
return drinkTea();
}).then((data) => {
arr.push(data);
console.log(arr);
console.timeEnd('promise');
});

打印结果:

1
2
3
4
5
D:\code\es6\promise-demo>node promiseForAsync.js
[ 'step1: 水刚刚烧开,可以泡茶了',
'step2: 水已经烧开了,开始泡茶',
'step3: 茶泡好了,正在喝茶' ]
promise: 8020.676ms

5. Promise.all 异步控制多个异步操作的执行顺序

promiseForAsync.js 中已经控制了多个异步操作的顺序,但这还不是我们想要的,异步顺序确实控制住了,但执行时间却变成了三个异步操作分别执行时间的和。

我们期待的结果,执行时间依然异步(不能超过三个异步操作中时间最长的那个时间,因为三个异步操作是同时进行的),执行顺序得到控制。

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
// promiseForAll.js

// 烧水:异步操作,需要 5 s
var boilWater = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step1: 水刚刚烧开,可以泡茶了');
}, 5000);
});
}

// 泡茶:异步操作,需要 2 s
var makeTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step2: 水已经烧开了,开始泡茶');
}, 2000);
});
}

// 喝茶:异步操作,需要 1 s
var drinkTea = function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('step3: 茶泡好了,正在喝茶');
}, 1000);
});
}

console.time('promise');
Promise.all([boilWater(), makeTea(), drinkTea()]).then((result) => {
console.log(result);
console.timeEnd('promise');
});

打印结果:

1
2
3
4
5
D:\code\es6\promise-demo>node promiseForAll.js
[ 'step1: 水刚刚烧开,可以泡茶了',
'step2: 水已经烧开了,开始泡茶',
'step3: 茶泡好了,正在喝茶' ]
promise: 5015.434ms

可以看到,这种写法执行顺序依然得到控制,而执行时间从 8s 变成了 5s,为什么这里不是刚好 5s,而是有零头的时间,这是由于使用了 setTimeout 模拟异步,这个方法在执行代码的过程中会浪费些许时间导致的。

二、异步编程之 async/await

1. 概述

上一篇提到的用 Promise 解决 js 异步编程的回调地狱,虽然相对于回调地狱是有改进,但是链式写法还是让人不爽,还是有回调函数。async/await 可谓是升级再升级,在 Promise 的基础上 完美 解决回调地狱的问题,说完美一点都不夸张,事实也确实如此,用同步的写法去写异步代码

async/await 是 ES7 里的语法,现在浏览器还在适应 ES6 的路上努力着,无法直接使用该语法,但是 Node 自 v8.0.0 版本之后就内置了该语法,也就是说,如果你的 Node 升级为 8.0 或者更高版本,在写 Node 代码的时候,不需要再像以往一样引入 async 或其它乱七八糟求兼容 async/await 的第三方依赖包,一句话,直接用。

2. 小试牛刀

第一种写法: 正常读取文件内容。

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

function start (src) {
fs.readFile(src, function (err, data) {
if (err) return console.log('读取文件内容出错啦~');
console.log(data.toString());
})
}

start('index.js')

第二种写法: 使用 async/await 语法编写读取文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

function readFile (src) {
return new Promise(function (resolve, reject) {
fs.readFile(src, function (err, data) {
if (err) reject(err);
resolve(data.toString());
});
});
}

async function start (src) {
const result = await readFile(src);
console.log(result);
}

start('index.js');

虽然代码行数变多了,但只看 start 方法,可以看到 async/await 写法中并没有用回调函数接收文件内容,而是 像调用同步方法一样直接返回结果 的形式。

事实上 readFile(src) 本身还是异步操作。只不过前面有 await 关键字,所以只有等到 readFile(src) 有返回值(无论是成功还是失败)才会继续下一行代码的运行。

3. 说明

3.1 异步函数必须返回 Promise 值

上述第二种方法中 fs.readFile() 本身是异步方法,又用 Promise 封装了一层写成 readFile 方法,这样返回值就是 Promise 值。

如果你想用 async/await,确保你的异步方法返回 Promise,如果不是,那就得自己封装一层也能用。

1
2
3
4
5
6
7
8
function readFile (src) {
return new Promise(function (resolve, reject) {
fs.readFile(src, function (err, data) {
if (err) reject(err);
resolve(data.toString());
});
});
}

3.2 async 和 await 必须同时出现

用到 await 关键字的方法必须前面用 async 修饰,也就是说 await 和 async 关键字必须同时出现在一个方法里;

1
2
3
async function start () {
await xxxxx();
}

3.3 写循环体时注意事项

await 不能写在数组 foreach、each、map、filter 等函数中。

1
2
3
4
5
6
7
// 报错
async function start () {
const list = [1, 2, 3];
list.map(async function (item) {
await xxx();
});
}

那如果要在循环内用 await,怎么办呢?用 for(let i = 0; i < arr.length; i++) 来写就没问题了。

1
2
3
4
5
6
7
// 不会报错,正常运行
async function start () {
const list = [1, 2, 3];
for(let i = 0; i < list.length; i++) {
await xxx();
}
}

用 for 写循环和用数组的 foreach、map 写玄幻有什么区别呢?为什么前者可以,后者就会报错呢?这是因为在 for 里面 this 还是指向最外层函数的,但是 foreach 和 map 中 this 的指向就会改变。

3.4 Express 路由写法

注意看 app.post 的第二个参数回调函数前面也是可以加 async 关键字的,这样回调函数里也就可以用 await 关键字了。

1
2
3
4
5
app.post('/gysw/book', async function (req, res) {
const data = req.body;
const result = await bookDao.create(data);
res.json(result);
});

3.5 多层 async/await 函数调用

用 async 修饰的 inner 方法,本身也变成了异步方法,所以 outer 方法在调用 inner 方法时也要写 async/await 才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function readFile () {
return new Promise(function () { //... });
}

async function inner () {
await readFile();
}

// 这样写会报错
// function outer () {
// inner();
// }

// 这样写才正确
async function outer () {
await inner();
}
首页
友链
归档
dkvirus
动态
RSS