作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
优素福·谢里夫的头像

Youssef Sherif

Youssef使用了React, Angular, NodeJS, 和Python来构建复杂的web应用程序, API services, 以及机器学习应用.

Previously At

34ML
Share

Node 8 is out! 事实上,Node 8已经发布了足够长的时间,可以看到一些可靠的实际使用情况. 它配备了快速的V8引擎和新功能, 包括异步/等待, HTTP/2, and async hooks. 但是它为你的项目准备好了吗? Let’s find out!

Editor’s note: 您可能已经意识到Node 10(代号为 Dubnium) is out, too. 我们选择专注于Node 8 (Carbon),原因有二:(1)Node 10刚刚进入其长期支持(LTS)阶段, (2)节点8的迭代比节点10的迭代更重要.

Node 8 LTS中的性能

我们首先来看一下这个非凡版本的性能改进和新特性. 一个主要的改进是Node的JavaScript引擎.

What exactly is 一个JavaScript引擎?

JavaScript引擎执行并优化代码. 它可以是标准解释器,也可以是将JavaScript编译为字节码的即时(JIT)编译器. Node使用的JS引擎.js都是JIT编译器,而不是解释器.

The V8 Engine

Node.js has used Google’s Chrome V8 JavaScript引擎, or simply V8,从一开始. 一些Node版本用于与更新版本的V8同步. 但是注意不要混淆V8和Node 8,因为我们在这里比较V8版本.

这很容易被绊倒, 因为在软件环境中,我们经常使用“v8”作为俚语,甚至是“version 8”的官方缩写,,所以有些人可能会把“Node V8”或“Node”混为一谈.js V8”和“NodeJS 8”,但我们在本文中避免了这一点,以帮助保持清晰: V8总是指引擎,而不是Node的版本.

V8 Release 5

Node 6使用V8 release 5作为它的JavaScript引擎. (Node 8的前几个点版本也使用了V8 release 5, 但是他们使用了比Node 6更新的V8版本.)

Compilers

V8版本5和更早的版本有两个编译器:

  • Full-codegen 是一个简单而快速的JIT编译器,但产生缓慢的机器码.
  • Crankshaft 是一个复杂的JIT编译器,产生优化的机器码.
Threads

实际上,V8使用了不止一种类型的线程:

  • 主线程获取代码,编译代码,然后执行代码.
  • 次要线程在主线程优化代码时执行代码.
  • 分析器线程通知运行时有关性能不佳的方法. 曲轴然后优化这些方法.
  • 其他线程管理垃圾收集.
Compilation Process

首先,Full-codegen编译器执行JavaScript代码. 当代码正在执行时, 分析器线程收集数据以确定引擎将优化哪些方法. 在另一个线程,曲轴优化这些方法.

Issues

上述方法有两个主要问题. 首先,它在建筑上很复杂. 第二,编译后的机器码消耗更多的内存. 所消耗的内存量与代码执行的次数无关. 即使是只运行一次的代码也会占用大量内存.

V8 Release 6

第一个使用V8版本6引擎的Node版本是Node 8.3.

在版本6中,V8团队构建了Ignition和TurboFan来缓解这些问题. 点火和涡扇分别取代全编码和曲轴.

新的架构更直接,消耗更少的内存.

Ignition 将JavaScript代码编译为字节码而不是机器码,节省了大量内存. Afterward, TurboFan优化编译器从这个字节码生成优化的机器码.

特定性能改进

让我们浏览一下Node 8中性能有所提高的领域.3+相对于旧的Node版本发生了变化.

Creating Objects

在Node 8中创建对象的速度大约快了5倍.3+ than in Node 6.

Function Size

V8引擎根据几个因素来决定一个功能是否应该被优化. 一个因素是函数大小. 小函数被优化,而长函数没有.

函数大小是如何计算的?

老式V8发动机的曲轴使用“字符计数”来确定功能大小. 函数中的空白和注释减少了它被优化的机会. 我知道这可能会让你感到惊讶,但在那个时候,一个评论可能会使速度降低10%左右.

In Node 8.不相关的字符,如空格和注释不会影响函数的性能. Why not?

因为新的TurboFan不计算字符来确定函数大小. 相反,它计算抽象语法树(AST)节点,因此它有效地只考虑 实际功能说明. Using Node 8.3+,你可以随心所欲地添加注释和空白.

Array-ifying Arguments

中的正则函数 JavaScript carry an implicit Array-like argument object.

What Does Array-like Mean?

The arguments object acts somewhat like an array. It has the length property but lacks Array的内置方法,如 forEach and map.

Here’s how the arguments object works:

function foo() {
    console.log(arguments[0]);
    //期望输出:a

    console.log(arguments[1]);
    //期望输出

    console.log(arguments[2]);
    //期望输出
}

foo("a", "b", "c");

那么我们如何转换 arguments object to an array? By using the terse Array.prototype.slice.call(arguments).

function test() {
    const r = Array.prototype.slice.call(arguments);
    console.log(r.map(num => num * 2));
}
test(1, 2, 3); // Expected output: [2, 4, 6]

Array.prototype.slice.call(arguments) 会影响所有Node版本的性能. 因此,通过a for 循环性能更好:

function test() {
    const r = [];
    For (index in arguments) {
        r.推动(参数(指标));
    }
    console.log(r.map(num => num * 2));
}

test(1, 2, 3); // Expected output [2, 4, 6]

The for 循环有点麻烦,不是吗? 我们可以使用扩展运算符,但它在Node 8中很慢.2 and down:

function test() {
    const r = [...arguments];
    console.log(r.map(num => num * 2));
}

test(1, 2, 3); // Expected output [2, 4, 6]

这种情况在Node 8中发生了变化.3+. 现在扩展执行得快多了,甚至比for循环还快.

部分应用程序(套用)和绑定

柯里化是将一个有多个参数的函数分解成一系列函数,其中每个新函数只接受一个参数.

假设我们有一个简单的 add function. 这个函数的柯里化版本只有一个参数, num1. 它返回一个接受另一个参数的函数 num2 并返回的和 num1 and num2:

函数add(num1, num2) {
    return num1 + num2;
}
add(4, 6); // returns 10

函数curriedAdd(num1) {
    返回函数(num2) {
        return num1 + num2;
    };
}
const add5 = curriedAdd(5);

add5(3); // returns 8

The bind 方法返回语法更简洁的柯里化函数.

函数add(num1, num2) {
    return num1 + num2;
}
const add5 = add.bind(null, 5);
add5(3); // returns 8

So bind 令人难以置信,但是在旧的Node版本中速度很慢. In Node 8.3+, bind 快得多,您可以使用它而不用担心任何性能影响.

Experiments

几个实验 已经进行了 比较Node 6和Node 8的性能. 注意,这些都是在Node 8上进行的.因此它们不包括上面提到的特定于Node 8的改进.多亏了V8版本6的升级.

Node 8中的服务器渲染时间比Node 6少了25%. 在大型项目中,服务器实例的数量可以从100个减少到75个. 这很惊人. 在Node 8中测试一组500个测试要快10%. Webpack的构建速度快了7%. 总的来说,结果显示在Node 8中有明显的性能提升.

Node 8 Features

速度并不是Node 8唯一的改进. 也许它还带来了一些方便的新功能 most importantly, async/await.

Node 8中的Async/Await

回调和承诺通常用于处理JavaScript中的异步代码. 回调因产生不可维护的代码而臭名昭著. 他们造成了混乱(具体称为 callback hell). 承诺将我们从回调地狱中解救了很长一段时间, 但是它们仍然缺乏同步代码的整洁性. Async/await是一种现代方法,它允许您 编写看起来像同步代码的异步代码.

而async/await可以在以前的Node版本中使用, 例如,它需要外部库和工具, 通过Babel进行额外的预处理. 现在它是本机可用的,开箱即用.

我将讨论async/await优于传统承诺的一些情况.

Conditionals

假设您正在获取数据,您将确定是否需要一个新的API调用 基于有效载荷. 看看下面的代码,看看这是如何通过“常规承诺”方法实现的.

const request = () => {
    return getData().then(data => {
        if (!data.car) {
            返回fetchForCar(数据.id).then(carData => {
                console.log(carData);
                return carData;
            });
        } else {
            console.log(data);
            return data;
        }
    });
};

正如您所看到的,仅仅从一个额外的条件来看,上面的代码看起来已经很混乱了. Async/await的嵌套更少:

const request = async () => {
    const data = await getData();
    if (!data.car) {
        const carData = await fetchForCar(data);
        console.log(carData);
        return carData;
    } else {
        console.log(data);
        return data;
    }
};

Error Handling

Async/await授予你在try/catch中处理同步和异步错误的权限. 假设您想解析来自异步API调用的JSON. 一个try/catch可以同时处理解析错误和API错误.

const request = async () => {
    try {
        console.日志(等待getData ());
    } catch (err) {
        console.log(err);
    }
};

Intermediate Values

如果一个承诺需要一个应该从另一个承诺解决的参数怎么办? 这意味着异步调用需要连续执行.

使用传统的承诺,你可能会得到这样的代码:

const request = () => {
    返回fetchUserData ()
        .then(userData => {
            返回fetchCompanyData(用户数据);
        })
        .then(companyData => {
            返回fetchretiingplan (userData, companyData);
        })
        .then(retiringPlan => {
            const retiringPlan = retiringPlan;
        });
};

Async/await在这种情况下发挥了作用,需要链式异步调用:

const request = async () => {
    const userData = await fetchUserData();
    const companyData = await fetchCompanyData(userData);
    const retiringPlan = await fetchRetiringPlan(userData, companyData);
};

Async in Parallel

如果您想并行调用多个异步函数该怎么办? 在下面的代码中,我们将等待 fetchHouseData 要解决,就打电话 fetchCarData. 尽管每一个都是独立的,但它们是顺序处理的. 您将等待两秒钟,等待这两个api解析完毕. This is not good.

函数fetchHouseData() {
    return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000));
}

函数fetchCarData() {
    return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000));
}

异步函数action() {
    const house = await fetchHouseData(); // Wait one second
    const car = await fetchCarData(); // ...然后再等一秒钟.
    console.原木(房子,汽车,“串联”);
}

action();

更好的方法是并行处理异步调用. 查看下面的代码,了解这是如何在async/await中实现的.

异步函数parallel() {
    houseDataPromise = fetchHouseData();
    carDataPromise = fetchCarData();
    const house = await houseDataPromise; // Wait one second for both
    const car = await carDataPromise;
    console.原木(房子,汽车,“平行”);
}

parallel();

并行处理这些调用只需要为两个调用等待一秒钟.

新的核心库功能

Node 8还带来了一些新的核心功能.

Copy Files

在Node 8之前,为了复制文件,我们过去常常创建两个流,并将数据从一个流传输到另一个流. 下面的代码显示了读流如何将数据输送到写流. 正如您所看到的,对于复制文件这样简单的操作,代码是混乱的.

Const fs = require('fs');
const rd = fs.createReadStream(“源文件.txt');
rd.on('error', err => {
    console.log(err);
});
const wr = fs.createWriteStream(“目标.txt');
wr.on('error', err => {
    console.log(err);
});
wr.On ('close', function(ex) {
    console.日志(“文件复制”);
});
rd.pipe(wr);

In Node 8 fs.copyFile and fs.copyFileSync 是否有新的方法可以减少复制文件的麻烦.

Const fs = require("fs");
fs.copyFile("firstFile.txt", "secondFile.txt", err => {
	if (err) {
		console.log(err);
	} else {
		console.log("File copied");
	}
});

承诺和回访

util.promisify 将普通函数转换为异步函数. 注意,输入的函数应该遵循公共Node.js callback style. 它应该接受一个回调作为最后一个参数,i.e., (error, payload) => { ... }.

Const {promisify} = require('util');
Const fs = require('fs');
const readFilePromisified = promisify(fs . conf).readFile);

Const file_path = process.argv[2];

readFilePromisified (file_path)
    .then((text) => console.log(text))
    .catch((err) => console.log(err));

As you could see, util.promisify has converted fs.readFile 到一个async函数.

另一方面,Node.js comes with util.callbackify. util.callbackify is the opposite of util.promisify:将async函数转换为Node.Js回调样式函数.

destroy 可读和可写的函数

The destroy 函数是一个销毁/关闭/中止可读或可写流的文档化方法:

Const fs = require('fs');
const file = fs.createWriteStream('./big.txt');

file.on('error', errors => {
    console.log(errors);
});

file.write(`New text.\n`);

file.destroy(['First Error', 'Second Error']);

上面的代码创建了一个名为 big.txt (如果它不存在的话) New text..

The Readable.destroy and Writeable.destroy Node 8中的函数发出a close event and an optional error event—destroy 并不一定意味着出了什么问题.

Spread Operator

展开运算符(又名 ...)可以在Node 6中工作,但只能用于数组和其他可迭代对象:

Const arr1 = [1,2,3,4,5,6]
const arr2 = [...arr1, 9]
console.Log (arr2) //期望输出:[1,2,3,4,5,6,9]

在Node 8中,对象也可以使用扩展操作符:

const userCarData = {
    type: 'ferrari',
    color: 'red'
};

const userSettingsData = {
    lastLoggedIn:“12/03/2019”,
    featuresPlan:“溢价”
};

const userData = {
    ...userCarData,
    name: 'Youssef',
    ...userSettingsData
};
console.log(userData);
/*期望输出:
    {
        type: 'ferrari',
        color: 'red',
        name: 'Youssef',
        lastLoggedIn:“12/03/2019”,
        featuresPlan:“溢价”
    }
*/

Node 8 LTS的实验特性

实验性功能不稳定,可能会被弃用,并且可能会随着时间的推移而更新. 中不要使用任何这些特性 production 直到它们变得稳定.

Async Hooks

异步钩子通过API跟踪在Node内部创建的异步资源的生命周期.

在进一步使用异步钩子之前,请确保理解了事件循环. This video might help. 异步钩子对于调试异步函数很有用. They have several applications; one of them is error stack traces for async functions.

看看下面的代码吧. Notice that console.log 是async函数吗. 因此,它不能在异步钩子中使用. fs.writeSync is used instead.

const asyncHooks = require('async_hooks');
Const fs = require('fs');
const init = (asyncId, type, triggerId) => fs.writeSync(1, ' ${type} \n ');
const asynchoks = asynchoks.createHook({init});
asyncHook.enable();

Watch this video 了解更多关于异步钩子的知识. In terms of a Node.我的指南, this article 通过一个说明性的应用程序帮助揭开异步钩子的神秘面纱.

节点8中的ES6模块

Node 8现在支持ES6模块,允许你使用以下语法:

import {UtilityService}./utility_service';

要在Node 8中使用ES6模块,需要执行以下操作.

  1. Add the ——experimental-modules 命令行标志
  2. 重命名文件扩展名 .js to .mjs

HTTP/2

HTTP/2是对不经常更新的HTTP协议和Node 8的最新更新.4+ 本机支持 在实验模式下. 它比它的前身HTTP/1更快、更安全、更高效.1. And 谷歌建议你使用它. 但它还能做什么呢?

Multiplexing

In HTTP/1.1、服务器一次只能对每个连接发送一个响应. 在HTTP/2中,服务器可以并行发送多个响应.

Server Push

服务器可以为单个客户机请求推送多个响应. 为什么这是有益的?? 以web应用程序为例. Conventionally,

  1. 客户端请求HTML文档.
  2. 客户机从HTML文档中发现所需的资源.
  3. 客户端为每个所需的资源发送HTTP请求. 例如,客户端为文档中提到的每个JS和CSS资源发送一个HTTP请求.

服务器推送特性利用了服务器已经知道所有这些资源的事实. 服务器将这些资源推送给客户端. 对于web应用程序的例子, 在客户端请求初始文档后,服务器推送所有资源. 这减少了延迟.

Prioritization

客户端可以设置优先级方案,以确定每个所需响应的重要性. 然后,服务器可以使用此方案来确定内存分配的优先级, CPU, bandwidth, 还有其他资源.

改掉坏习惯

Since HTTP/1.不允许多路复用, 使用了一些优化和变通方法来掩盖缓慢的速度和文件加载. 不幸的是,这些技术导致内存消耗增加和渲染延迟:

  • 域分片:使用多个子域,以便分散连接并并行处理.
  • 结合CSS和JavaScript文件来减少请求的数量.
  • Sprite maps: 组合图像文件 减少HTTP请求.
  • 内联:CSS和JavaScript直接放在HTML中,以减少连接的数量.

现在使用HTTP/2,您可以忘记这些技术并专注于您的代码.

但是如何使用HTTP/2?

大多数浏览器只通过安全的SSL连接支持HTTP/2. This article 可以帮助您配置自签名证书吗. Add the generated .crt file and .key 目录中的文件 ssl. 然后,将下面的代码添加到名为 server.js.

记得使用 --expose-http2 在命令行中启用此特性. I.e. 我们示例中的run命令是 node server.js --expose-http2.

Const http2 = require('http2');
Const path = require('path');
Const fs = require('fs');

const PORT = 3000;

const secureServerOptions = {
    cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')),
    key: fs.readFileSync(path.join(__dirname, './ssl/server.key'))
};

Const server = http2.createSecureServer(secureServerOptions, (req, res) => {
    res.statusCode = 200;
    res.end('来自Toptal的Hello ');
});

server.listen(
    PORT,
    err =>
        err
            ? console.error(err)
            : console.日志('服务器正在监听端口${port} ')
);

当然,还有Node 8, Node 9, Node 10等等. 仍然支持旧的HTTP 1.1 -官方节点.js documentation on 标准HTTP事务 不会陈腐很久的. 但是如果你想使用HTTP/2,你可以使用 this Node.js guide.

那么,我应该使用Node吗.js 8 in the End?

Node 8带来了性能改进和async/await等新特性, HTTP/2, and others. 端到端实验表明,Node 8比Node 6快25%左右. This leads to 节省大量成本. 所以对于绿地项目来说,绝对是这样! 但是对于现有的项目,你应该 update Node?

这取决于您是否需要更改大部分现有代码. This document 如果您是从Node 6过来的,则列出所有Node 8的破坏性更改. 请记住,通过重新安装所有的项目来避免常见问题 npm 使用最新的Node 8版本. 此外,始终使用相同的Node.Js版本在开发机器上和在生产服务器上一样. Best of luck!

了解基本知识

  • What is Node.js?

    Node.js是一个开源的JavaScript运行时,可以在Windows等各种平台上运行, Linux, and Mac OS X. Node.js是建立在Chrome的V8 JavaScript引擎.

  • What is Node.js used for?

    Node.js用于在服务器端运行JavaScript. 与其他运行时和语言一样,Node.Js可以处理服务器上的文件, 与数据库通信, 生成动态页面内容, 构建RESTful API服务, etc.

  • Why use Node.js?

    Node.Js在处理实时双向通信和开箱即用运行异步代码方面更胜一筹. 这有助于创建高度可伸缩的高性能应用程序. 此外,已经知道JavaScript的前端开发人员不需要学习另一种语言.

  • LTS是什么意思?

    LTS代表“长期支持”.在这种情况下,Node.js 8处于LTS阶段,这意味着它是最稳定的版本之一.

就这一主题咨询作者或专家.
Schedule a call
优素福·谢里夫的头像
Youssef Sherif

Located in 沙迦,阿拉伯联合酋长国

Member since May 21, 2018

About the author

Youssef使用了React, Angular, NodeJS, 和Python来构建复杂的web应用程序, API services, 以及机器学习应用.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

34ML

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.