在今年 11 月份 Node.js 发布 v8.x LTS 版本之后,终于可以不用借助额外的工具就可以使用 ESNext 标准中的
来进行异步编程,彻底改变了我们的编程习惯。以前被人诟病的 回调地狱 将不复存在,而且再也不需要使用 Generator 这种 撇脚 的方式了(请阅读我于去年写的文章 《基于 Generator 与 Promise 的异步编程解决方案》 )。
- async function
在此之前,为了在 Node v6.x 及更早的版本中使用
,我们需要使用 Babel 或者 TypeScript 这样的工具将代码转换成使用
- async function
或者
- callback
的方式。而 根据以往的经验,这些被转换过的代码执行效率都比较低,比如 Node v4.x 以前的 Generator 和之后的 原生 Promise 。因此,为了让我们更有信心地使用
- Generator
,我做了一些简单的性能测试,请看下文。
- async function
原始的代码使用 TypeScript 编写,然后通过命令
将其分别转换为不同的目标代码(文件: build.sh ):
- tsc --target esnext|es6|es5
方式,文件: esnext.js
- async function
方式,文件: es6.js
- Generator
方式(注意: 使用此方式会生成很多无用的代码,其主要是用于模拟 Generator,比我们自己手写的 callback 代码低效很多 ),文件: es5.js
- callback
被编译后的代码会分别在 Node.js 各个主要版本上执行: v4.8.7 、 6.12.2 、 8.9.3 和 9.3.0 (为了描述方便,下文会简单描述为 4.x、6.x、8.x 和 9.x),其中由于 v4.x 和 v6.x 不支持
则不需要执行该测试。执行花费的时间取开始和结束的
- async function
之差(单位为秒),内存占用取
- process.uptime()
(单位为 MB)。
- process.memoryUsage().rss / 1000000
以下是 TypeScript 源码:
- "use strict";
- function add(n) : Promise < number > {
- return new Promise((resolve, reject) = >{
- resolve(n + 1);
- });
- }
- async
- function call() {
- const a = await add(1);
- const b = await add(2);
- const c = await add(3);
- const d = await add(4);
- return a + b + c + d === 14;
- }
- async
- function test(n: number) {
- const version = `node $ {
- process.version
- }`;
- const name = __filename.split(/\\|\//).pop().slice(0, -3);
- const promise = Promise.toString().indexOf("[native code]") !== -1 ? "ES6 Promise": "bluebird Promise";
- const title = `$ {
- version
- }
- $ {
- name
- }
- with $ {
- promise
- } - test $ {
- n
- }
- times`;
- const time = process.uptime();
- for (let i = 0; i < n; i++) {
- await call();
- }
- console.log("%s - %ds - %dMB", title, (process.uptime() - time).toFixed(2), (process.memoryUsage().rss / 1000000).toFixed(1));
- }
- const K = 1000;
- let num = parseInt(process.env.NUM, 10);
- if (isNaN(num) || !(num > 0)) {
- num = 100;
- }
- test(num * K);
说明:
模拟一个简单的异步操作,通过
- add()
实现,返回输入参数
- Promise
的值加
- n
- 1
模拟一次异步调用流程,包含
- call()
次
- 4
异步操作
- add()
是测试流程控制,包含循环多次测试
- test()
并返回计算总花费时间和内存占用,并打印出结果
- call()
我们首先看看执行 100 万次
时,使用
- call()
和
- callback
在各个版本上执行的情况:
- Generator
Node.js 版本 | 异步方式 | 花费时间(s) | 内存占用(MB) |
---|---|---|---|
v4.x | callback | 45 | 238.9 |
同上 | Generator | 24.02 | 71.5 |
v6.x | callback | 20.02 | 61.9 |
同上 | Generator | 22.62 | 69 |
v8.x | callback | 5.57 | 85.8 |
同上 | Generator | 5.72 | 96.3 |
v9.x | callback | 6.4 | 91.9 |
同上 | Generator | 6.59 | 96.8 |
由上表可以看出,从 v6.x 到 v8.x 其花费的时间和内存占用都降低了很多(v9.x 由于是非稳定版本的原因,测试结果有较大偏差相差很大是可以理解的), 说明新版本的 Node.js 性能都有了很大的提升。
我们关心的第一个问题是: 直接使用 async function 会比转成相应的 Genrator 代码高效吗?
Node.js 版本 | 异步方式 | 花费时间(s) | 内存占用(MB) |
---|---|---|---|
v4.x | Generator | 24.02 | 71.5 |
v6.x | Generator | 22.62 | 69 |
v8.x | Generator | 5.72 | 96.3 |
同上 | async | 1.76 | 31.5 |
v9.x | Generator | 6.59 | 96.8 |
同上 | async | 1.91 | 31.5 |
答案是肯定的。 在 Node v8.x 上使用 async function 比转成相应的 Generator 代码执行性能提高了 3 倍。
一直以来我们都有一个印象,原生 Promise 性能很差,一般会使用
这种第三方 Promise 实现来代替。那么第二个问题来了: 使用 bluebird 代替原生 Promise 会不会更高效?
- bluebird
(方法:执行测试程序前通过
替换全局的 Promise 对象)
- global.Promise = require("bluebird")
Node.js 版本 | 异步方式 | 花费时间(s) | 内存占用(MB) |
---|---|---|---|
v4.x | Generator | 10.54 | 86.2 |
v6.x | Generator | 8.17 | 78.9 |
v8.x | Generator | 5.46 | 90.3 |
同上 | async | 16.25 | 34 |
v9.x | Generator | 5.83 | 93.7 |
同上 | async | 15.39 | 34.7 |
对比上面两张表格可以看出,在 v4.x 和 v6.x 的时候,使用 bluebird 差不多有 2 倍的性能提升。但是,在 v8.x 之后却是相反的。因此, Node.js v8.x 的原生 Promise 已经得到了很大的优化,可以不需要使用 bluebird 这样的第三方 Promise 库;如果使用了 async function,替换原生的 Promise 反而会大大降低性能。
好了,最最关键的问题来了: 使用 async function 与 callback 方式做同样的事情性能相差多少?
以下是我将上文的代码 经过简单的转换而成的 callback 写法:
- "use strict";
- function add(n, callback) {
- process.nextTick(() = >callback(null, n + 1));
- }
- function call(callback) {
- add(1, (err, a) = >{
- if (err) return callback(err);
- add(2, (err, b) = >{
- if (err) return callback(err);
- add(3, (err, c) = >{
- if (err) return callback(err);
- add(4, (err, d) = >{
- if (err) return callback(err);
- callback(null, a + b + c + d === 14);
- });
- });
- });
- });
- }
- function test(n) {
- const version = `node $ {
- process.version
- }`;
- const name = __filename.split(/\\|\//).pop().slice(0, -3);
- const title = `$ {
- version
- }
- $ {
- name
- }
- with callback - test $ {
- n
- }
- times`;
- const time = process.uptime();
- const done = () = >{
- console.log("%s - %ds - %dMB", title, (process.uptime() - time).toFixed(2), (process.memoryUsage().rss / 1000000).toFixed(1));
- };
- let i = 0;
- const next = err = >{
- if (err) throw err;
- if (i < n) {
- i++;
- process.nextTick(() = >call(next));
- } else {
- done();
- }
- };
- next();
- }
- const K = 1000;
- let num = parseInt(process.env.NUM, 10);
- if (isNaN(num) || !(num > 0)) {
- num = 100;
- }
- test(num * K);
说明:
,如果没有错误则
- callback(err, ret)
,
- err = null
表示返回值
- ret
都需要放在
- call()
回调函数里面,主要是用于模拟一个异步操作,否则程序会因为调用堆栈过深而报错
- process.nextTick()
以下是执行结果:
Node.js 版本 | 花费时间(s) | 内存占用(MB) |
---|---|---|
v4.x | 0.64 | 54.8 |
v6.x | 0.65 | 54.8 |
v8.x | 1.04 | 27.5 |
v9.x | 1.03 | 27.5 |
使用
编写的代码在 Node.js v8.x 花费的时间是 1.76s ,内存占用是 31.5MB ,与使用
- async function
编写的代码相比, 数值相差并不大(执行 100 万次时间相差不足 1 秒),属于可以接受范围 。
- callback
以上可以算是一个 不太严谨 的测试方案,并没有全面地测试不同实现方式对结果的影响,也没有重复执行多次的测试来尽量减少结果偏差。但无论怎样,通过这些测试结果我们还是可以知道:
新的 ES 语法大大简化了异步编程的难度,而随着 Node.js 版本的升级,刚开始担心的那些性能问题也终将化为浮云。
来源: http://www.tuicool.com/articles/rUfiqaR