node.js开发系列[5]-异步编程

回调

Node.js 中回调的使用自不必多说,简直所有的 API 都是回调啦。比如最简单的一个读取文件

1
2
3
4
5
6
7
const fs = require("fs");

fs.readFile("./a.txt", (err, data) => {
if (!err) {
console.log(data.name);
}
});

Promise

回调很不友好,容易形成 callback hell。所以出现了 Promise 标准,如果环境不支持 Promise,可以使用 bluebird 这些实现了 Promise A+ 规范的库来代替 Promise。如:

1
const Promise = require("bluebird");

不过,现在 Node 和现代浏览器中都已经实现了原生的 Promise。如果底层 API 不是 promise 的,我们可以通过 Promise 类,把 callback 的 API 封装为 promise。我们演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require("fs");
// promise化
function readFileByPromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// 用promise方式调用
readFileByPromise("./package.json")
.then((data) => {
data = JSON.parse(data);
console.log("读取成功", data);
})
.catch((err) => {
console.log("读取失败", err);
});

回调异步的 Promise 化

由于现在 ES6 ES7 发展迅速, Promise 甚至 async await 越来越被开发者喜欢,但 Node.js 中默认的内置 API 仍然是传统的回调函数方式调用。为了能使用 Promise 的方式来使用这些 API, 我们可以借助 Node.js 内置的一个 promisify 函数对已有的 API 进行包装,包装后就可以使用 Promise 的方式调用了。

util.promisify 将 Node 中经典的回调函数调用方式修改为 Promise 调用的方式。我们来看下示例:

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

const stat = util.promisify(fs.stat);
stat(".")
.then((stats) => {
// Do something with `stats`
})
.catch((error) => {
// Handle the error.
});

如果待转换的函数自身拥有 util.promisify.custom 这个 Symbol 类型的属性,则 promisify 函数会自动使用该属性定义的 Promise 实现。

Generator 异步任务的自动运行

要学习 generator 处理异步,我们先了解下 迭代器和 generator 的概念

迭代器

迭代器是指的具有 next 方法的一个对象, 每次调用 next 迭代这个对象都可以反映对象内部的一个状态。我们可以自己实现一个类似迭代器的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getIterator() {
const arr = ["1", "2", "3"];
let i = 0;
return {
next() {
if (i < arr.length) {
return { done: false, value: arr[i] };
} else {
return { done: true };
}
},
};
}

这样调用 getIterator 就能返回一个迭代器类型的对象,然后调用 next 即可输出他的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
let it = getIterator()
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

// 下面是输出结果
{ done: false, value: '1' }
{ done: false, value: '2' }
{ done: false, value: '3' }
{ done: true }
{ done: true }

Generator

Generator 我们可以称之为生成器, 这个在我博客中有专门讲过 generator。迭代器就是由 “生成器” 来生成的,上面一节中的 getIterator 函数就可以认为是一个 生成器。

在 ES6 里面有专门的一个 Generator 类型的函数,就是用来生成迭代器的。我们来实现相同的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义生成器
function *logArr(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i]
}
}
const it = logArr(['1', '2', '3']) // 创建迭代器
// 迭代这个迭代器
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
// 以下是控制台输出
{ value: '1', done: false }
{ value: '2', done: false }
{ value: '3', done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }

另外,for…of 操作符,就是用来遍历实现了 next 函数的迭代器的。如上的实例可以改写为:

1
2
3
4
5
6
7
8
9
function* logArr(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}
const it = logArr(["1", "2", "3"]); // 创建迭代器
for (let item of it) {
console.log(item);
}

for…of 只会迭代出迭代器的 value,并且遇到 done:true 就停止迭代。

co 是 TJ 大神的神作。Koa, exporess, jade 等都 TJ 大神的作品。

co 是一个很简单的库,它的 API 就只有一个函数,就叫做 co 函数。co 是用来执行一个 Generator 或 generator 函数的一个库,并且最后会返回一个 Promise。

generator 里面 yield 后面可以支持的类型有很多,不过 co 由于是针对异步自动执行的库,因此它只支持以下几种类型:

1
2
3
4
5
6
promises
thunks (functions)
array (parallel execution)
objects (parallel execution)
generators (delegation)
generator functions (delegation)

使用 co 来写一个例子:

1
2
3
4
5
6
7
8
9
10
const co = require("co");
const fetch = require("node-fetch");

co(function* () {
const res = yield fetch("https://api.douban.com/v2/movie/1292052"); // 获取豆瓣电影信息
// res是一个promise,而调用json方法又是返回一个promise,因此需要继续yield
const jsonMovie = yield res.json();
console.log(jsonMovie.summary);
return jsonMovie.summary;
});

node-fetch 是 Node.js 中一个模拟浏览器中的 fetch API 的库,执行上面的代码可以看到控制台输出了电影的摘要。其实 generator 会返回一个 promise,因此我们可以在外面打印这个结果:

1
2
3
4
5
6
7
8
9
const taskResult = co(function* () {
const res = yield fetch("https://api.douban.com/v2/movie/1292052"); // 获取豆瓣电影信息
// res是一个promise,而调用json方法又是返回一个promise,因此需要继续yield
const jsonMovie = yield res.json();
return jsonMovie.summary;
});
taskResult.then((data) => {
console.log(data.summary);
});

co 的实现我们在 Koa 原理讲解中会稍微细致点的讲解。这里我们用简单的代码来模拟下 co 的实现,有益于我们理解 generator 的执行过程. 还是以上面的例子为例,我们自己实现一个 run 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const run = function (gen) {
const it = gen();
let promiseRel1 = it.next();
promiseRel1 = promiseRel1.value;
promiseRel1.then((data1) => {
let promiseRel2 = it.next(data1);
promiseRel2 = promiseRel2.value;
promiseRel2.then((data2) => {
let promiseRel3 = it.next(data2);
console.log(promiseRel3);
// promiseRel3 已经是 {done: true, value: 'xxx'}; 此时 value是generator中return的值,不再是promise
});
});
};

// 用自己的run函数来执行上文的generator
run(function* () {
const res = yield fetch("https://api.douban.com/v2/movie/1292052"); // 获取豆瓣电影信息
// res是一个promise,而调用json方法又是返回一个promise,因此需要继续yield
const jsonMovie = yield res.json();
return jsonMovie.summary;
});

可见,generator 要想自动运行,其实依赖于 yield 返回一个 promise. co 在内部会对 array, object, 等类型都调用其内部的 toPromise 进行了封装或递归调用。

对于 Node.js 来说,可以使用上文说过的 util.promisify 函数来将要执行的异步任务 promise 化,然后使用 generator 进行异步开发。

async

async 是对 generator 和 co 这种执行机制的原生封装。其底层就是类似 co 这样的执行机制。只是该语法糖让我们写起来比 generator 更加简洁, 语义上更容易理解。以一个读写文件的例子为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require("fs");
const util = require("util");
// promise化
function readFileByPromise(path) {
return util.promisify(fs.readFile)(path);
}
// 声明 async 函数
async function readFileByAsync(path) {
const data = await readFileByPromise(path);
return JSON.parse(data);
}
// 调用 async 函数
readFileByAsync("./package.json").then(console.log);

可以只要把 generator 函数声明换做 async 函数声明,把 yield 换做 await,那么 async 的使用方式跟 generator 完全一致。