是时候使用 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 中的常规函数带有一个隐式的类似Array
的argument
对象。
类Array
是什么意思?
arguments
对象的行为有点像数组。 它具有length
属性,但缺少Array
的内置方法,如forEach
和map
。
以下是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
并返回num1
和num2
之和:
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.copyFile
和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 回调样式。 它应该将回调作为最后一个参数,即(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.callbackify
。 util.callbackify
与util.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.destroy
和Writeable.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 模块,您需要执行以下操作。
- 将
--experimental-modules
标志添加到命令行 - 将文件扩展名从
.js
重命名为.mjs
HTTP/2
HTTP/2 是对不经常更新的 HTTP 协议的最新更新,Node 8.4+ 在实验模式下原生支持它。 它比其前身 HTTP/1.1 更快、更安全、更高效。 谷歌建议你使用它。 但它还有什么作用?
多路复用
在 HTTP/1.1 中,服务器一次只能为每个连接发送一个响应。 在 HTTP/2 中,服务器可以并行发送多个响应。
服务器推送
服务器可以为单个客户端请求推送多个响应。 为什么这是有益的? 以 Web 应用程序为例。 按照惯例,
- 客户端请求一个 HTML 文档。
- 客户端从 HTML 文档中发现所需的资源。
- 客户端为每个所需资源发送一个 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 后端教程