是时候使用 Node 8 了吗?

已发表: 2022-03-11

节点 8 出来了! 事实上,Node 8 现在已经出现了足够长的时间,可以看到一些可靠的实际使用情况。 它带有一个快速的新 V8 引擎和新特性,包括 async/await、HTTP/2 和 async 钩子。 但它为您的项目做好准备了吗? 让我们来了解一下!

编者注:您可能已经知道 Node 10(代号为Dubnium )也已发布。 我们选择关注节点 8 ( Carbon ) 有两个原因:(1) 节点 10 刚刚进入其长期支持 (LTS) 阶段,(2) 节点 8 标志着比节点 10 更重要的迭代.

Node 8 LTS 中的性能

我们将首先了解这个非凡版本的性能改进和新功能。 一个主要的改进领域是 Node 的 JavaScript 引擎。

到底什么JavaScript 引擎?

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

V8 引擎

Node.js 从一开始就使用 Google 的Chrome V8 JavaScript 引擎,或者简称V8 。 一些 Node 版本用于与较新版本的 V8 同步。 但请注意不要将 V8 与 Node 8 混淆,因为我们在这里比较 V8 版本。

这很容易被绊倒,因为在软件上下文中,我们经常使用“v8”作为俚语,甚至是“版本 8”的官方缩写,所以有些人可能会将“Node V8”或“Node.js V8”与“NodeJS 8”混为一谈,”但我们在整篇文章中都避免了这一点,以帮助保持清晰: V8 永远意味着引擎,而不是 Node.js 的版本。

V8 第 5 版

Node 6 使用 V8 第 5 版作为其 JavaScript 引擎。 (Node 8 的前几个版本也使用 V8 版本 5,但它们使用的 V8 版本比 Node 6 更新。)

编译器

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

  • Full-codegen是一种简单快速的 JIT 编译器,但生成的机器代码很慢。
  • Crankshaft是一种复杂的 JIT 编译器,可以生成优化的机器代码。
线程

在内心深处,V8 使用了不止一种类型的线程:

  • 主线程获取代码,编译它,然后执行它。
  • 辅助线程在主线程优化代码时执行代码。
  • 探查器线程通知运行时有关性能不佳的方法。 然后曲轴优化这些方法。
  • 其他线程管理垃圾收集。
编译过程

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

问题

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

V8 版本 6

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

在第 6 版中,V8 团队构建了 Ignition 和 TurboFan 来缓解这些问题。 Ignition 和 TurboFan 分别替换了 Full-codegen 和 CrankShaft。

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

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

具体的性能改进

让我们看看 Node 8.3+ 中的性能相对于旧 Node 版本发生了哪些变化。

创建对象

在 Node 8.3+ 中创建对象的速度大约是 Node 6 中的五倍。

函数大小

V8 引擎根据几个因素决定是否应优化功能。 一个因素是函数大小。 小函数得到优化,而长函数则没有。

函数大小如何计算?

旧 V8 引擎中的曲轴使用“字符数”来确定函数大小。 函数中的空格和注释会降低优化的机会。 我知道这可能会让您大吃一惊,但当时,评论可能会使速度降低约 10%。

在 Node 8.3+ 中,空格和注释等无关字符不会损害函数性能。 为什么不?

因为新的 TurboFan 不计算字符来确定函数大小。 相反,它计算抽象语法树 (AST) 节点,因此它只考虑实际的功能指令。 使用 Node 8.3+,您可以根据需要添加注释和空格。

Array化参数

JavaScript 中的常规函数​​带有一个隐式的类似Arrayargument对象。

Array是什么意思?

arguments对象的行为有点像数组。 它具有length属性,但缺少Array的内置方法,如forEachmap

以下是arguments对象的工作方式:

 function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");

那么我们如何将arguments对象转换为数组呢? 通过使用简洁的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 版本的性能。 因此,通过for循环复制键执行得更好:

 function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]

for循环有点麻烦,不是吗? 我们可以使用扩展运算符,但在 Node 8.2 及以下版本中它很慢:

 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 循环还要快。

部分应用(Currying)和绑定

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

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

 function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8

bind方法返回一个语法更简洁的柯里化函数。

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

所以bind是不可思议的,但是在旧的 Node 版本中它很慢。 在 Node 8.3+ 中, bind更快,您可以使用它而不必担心任何性能损失。

实验

已经进行了几个实验来比较节点 6 和节点 8 的性能。 请注意,这些是在 Node 8.0 上进行的,因此它们不包括上面提到的 Node 8.3+ 特有的改进,这要归功于其 V8 版本 6 升级。

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

节点 8 功能

速度并不是 Node 8 中唯一的改进。它还带来了几个方便的新特性——也许最重要的是, async/await

节点 8 中的异步/等待

回调和承诺通常用于处理 JavaScript 中的异步代码。 回调因产生不可维护的代码而臭名昭著。 它们在 JavaScript 社区中引起了混乱(特别是回调地狱)。 Promise 将我们从回调地狱中拯救了很长时间,但它们仍然缺乏同步代码的简洁性。 Async/await 是一种现代方法,允许您编写看起来像同步代码的异步代码。

虽然 async/await 可以在以前的 Node 版本中使用,但它需要外部库和工具——例如,通过 Babel 进行额外的预处理。 现在它可以在本地使用,开箱即用。

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

条件句

假设您正在获取数据,您将根据有效负载确定是否需要新的 API 调用。 看看下面的代码,看看这是如何通过“传统承诺”方法完成的。

 const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.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; } };

错误处理

Async/await 授予您处理 try/catch 中的同步和异步错误的权限。 假设您要解析来自异步 API 调用的 JSON。 单个 try/catch 可以处理解析错误和 API 错误。

 const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };

中间值

如果一个 Promise 需要一个应该从另一个 Promise 解决的参数怎么办? 这意味着需要串行执行异步调用。

使用传统的 Promise,您最终可能会得到如下代码:

 const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(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); };

并行异步

如果要并行调用多个异步函数怎么办? 在下面的代码中,我们将等待fetchHouseData解析,然后调用fetchCarData 。 尽管它们中的每一个都相互独立,但它们是按顺序处理的。 您将等待两秒钟让两个 API 解析。 这是不好的。

 function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();

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

 async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();

并行处理这些调用让您只需等待一秒钟即可完成这两个调用。

新的核心库函数

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

复制文件

在 Node 8 之前,为了复制文件,我们曾经创建两个流并将数据从一个流传输到另一个。 下面的代码显示了读取流如何将数据传输到写入流。 正如您所看到的,对于复制文件这样简单的操作,代码是杂乱无章的。

 const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);

在 Node 8 中, fs.copyFilefs.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 回调样式。 它应该将回调作为最后一个参数,即(error, payload) => { ... }

 const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));

如您所见, util.promisify已将fs.readFile转换为异步函数。

另一方面,Node.js 带有util.callbackifyutil.callbackifyutil.promisify相反:它将异步函数转换为 Node.js 回调样式函数。

可读可写的destroy函数

Node 8 中的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. .

Node 8 中的Readable.destroyWriteable.destroy函数会发出一个close事件和一个可选error事件—— destroy并不一定意味着任何错误。

扩展运算符

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

 const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]

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

 const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */

Node 8 LTS 中的实验功能

实验性功能不稳定,可能会被弃用,并且可能会随着时间的推移而更新。 在它们变得稳定之前,不要在生产中使用任何这些功能。

异步钩子

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

在进一步使用异步钩子之前,请确保您了解事件循环。 这个视频可能会有所帮助。 异步钩子对于调试异步函数很有用。 它们有几个应用程序; 其中之一是异步函数的错误堆栈跟踪。

看看下面的代码。 请注意, console.log是一个异步函数。 因此它不能在异步钩子中使用。 而是使用fs.writeSync

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

观看此视频以了解有关异步挂钩的更多信息。 具体就 Node.js 指南而言,本文通过一个说明性的应用程序帮助揭开异步挂钩的神秘面纱。

Node 8 中的 ES6 模块

Node 8 现在支持 ES6 模块,使您能够使用以下语法:

 import { UtilityService } from './utility_service';

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

  1. --experimental-modules标志添加到命令行
  2. 将文件扩展名从.js重命名为.mjs

HTTP/2

HTTP/2 是对不经常更新的 HTTP 协议的最新更新,Node 8.4+ 在实验模式下原生支持它。 它比其前身 HTTP/1.1 更快、更安全、更高效。 谷歌建议你使用它。 但它还有什么作用?

多路复用

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

服务器推送

服务器可以为单个客户端请求推送多个响应。 为什么这是有益的? 以 Web 应用程序为例。 按照惯例,

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

服务器推送功能利用了服务器已经知道所有这些资源的事实。 服务器将这些资源推送到客户端。 因此,对于 Web 应用程序示例,在客户端请求初始文档后,服务器会推送所有资源。 这减少了延迟。

优先级

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

改掉旧的坏习惯

由于 HTTP/1.1 不允许多路复用,因此使用了一些优化和变通方法来掩盖缓慢的速度和文件加载。 不幸的是,这些技术会导致 RAM 消耗增加和渲染延迟:

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

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

但是如何使用 HTTP/2?

大多数浏览器仅通过安全的 SSL 连接支持 HTTP/2。 本文可以帮助您配置自签名证书。 在名为ssl的目录中添加生成的.crt文件和.key文件。 然后,将以下代码添加到名为server.js的文件中。

请记住在命令行中使用--expose-http2标志来启用此功能。 即我们示例的运行命令是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('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );

当然,Node 8、Node 9、Node 10 等仍然支持旧的 HTTP 1.1——关于标准 HTTP 事务的官方 Node.js 文档不会过时很长时间。 但是如果你想使用 HTTP/2,你可以更深入地阅读这个 Node.js 指南。

那么,我到底应该使用 Node.js 8 吗?

Node 8 带来了性能改进以及 async/await、HTTP/2 等新功能。 端到端实验表明,Node 8 比 Node 6 快约 25%。这可以节省大量成本。 所以对于新建项目,绝对! 但是对于现有的项目,你应该更新 Node 吗?

这取决于您是否需要更改大部分现有代码。 如果您来自 Node 6,本文档列出了所有 Node 8 重大更改。请记住,通过使用最新的 Node 8 版本重新安装项目的所有npm包来避免常见问题。 此外,始终在开发机器上使用与生产服务器上相同的 Node.js 版本。 祝你好运!

有关的:
  • 为什么我会使用 Node.js? 个案教程
  • 调试 Node.js 应用程序中的内存泄漏
  • 在 Node.js 中创建安全 REST API
  • Cabin Fever 编码:Node.js 后端教程