回调
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");
function readFileByPromise(path) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err); else resolve(data); }); }); }
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) => { }) .catch((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"); 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"); 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); }); }); };
run(function* () { const res = yield fetch("https://api.douban.com/v2/movie/1292052"); 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");
function readFileByPromise(path) { return util.promisify(fs.readFile)(path); }
async function readFileByAsync(path) { const data = await readFileByPromise(path); return JSON.parse(data); }
readFileByAsync("./package.json").then(console.log);
|
可以只要把 generator 函数声明换做 async 函数声明,把 yield 换做 await,那么 async 的使用方式跟 generator 完全一致。