个性化阅读
专注于IT技术分析

现在该使用Node 8了吗?

点击下载

本文概述

Node 8出局了!实际上, Node 8的使用时间已经足够长, 足以看到实际的实际使用情况。它带有快速的新V8引擎和新功能, 包括异步/等待, HTTP / 2和异步挂钩。但是为你的项目准备好了吗?找出答案吧!

编者注:你可能还知道Node 10(代号为Dubnium)也已用完。我们选择关注Node 8(碳)有两个原因:(1)Node 10刚刚进入长期支持(LTS)阶段, 并且(2)Node 8的迭代比Node 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版本。

V8版本5

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

编译器

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

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

深入来讲, V8使用了多种类型的线程:

  • 主线程获取代码, 对其进行编译, 然后执行。
  • 辅助线程执行代码, 而主线程优化代码。
  • 探查器线程通知运行时有关无效方法的信息。曲轴然后优化这些方法。
  • 其他线程管理垃圾回收。
编译过程

首先, Full-codegen编译器执行JavaScript代码。在执行代码时, 探查器线程将收集数据以确定引擎将优化的方法。在另一个方面, 曲轴优化了这些方法。

问题

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

V8版本6

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

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

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

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

具体性能改进

我们来看一下Node 8.3+中的性能相对于旧版Node而言发生了变化的地方。

创建对象

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

功能大小

V8引擎基于几个因素来决定是否应优化功能。一个因素是功能大小。小功能已优化, 长功能未优化。

函数大小如何计算?

旧V8发动机的曲轴使用”字符数”来确定功能尺寸。函数中的空格和注释减少了对其进行优化的机会。我知道这可能会让你感到惊讶, 但那时, 发表评论可能会使速度降低10%。

在Node 8.3+中, 不相关的字符(例如空格和注释)不会影响功能性能。为什么不?

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

数组化参数

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

类数组是什么意思?

arguments对象的行为有点像数组。它具有length属性, 但缺少Array的内置方法(例如forEach和map)。

参数对象的工作方式如下:

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正在分解一个将多个参数合并为一系列函数的函数, 其中每个新函数仅包含一个参数。

假设我们有一个简单的添加功能。该函数的当前版本采用一个参数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

因此, 绑定是令人难以置信的, 但是在较旧的Node版本中, 绑定速度很慢。在Node 8.3+中, 绑定要快得多, 你可以使用它而不必担心任何性能下降。

实验

已经进行了一些实验, 以在较高水平上比较Node 6和Node 8的性能。请注意, 这些操作是在Node 8.0上进行的, 因此由于V8版本6的升级, 因此并未包括上述针对Node 8.3+的改进。

Node 8中的服务器渲染时间比Node 6中的少25%。在大型项目中, 服务器实例的数量可以从100个减少到75个。这真是令人惊讶。在Node 8中测试500个测试套件的速度提高了10%。 Webpack构建速度提高了7%。通常, 结果表明Node 8的性能有了显着提高。

Node 8功能

速度并不是Node 8的唯一改进。它还带来了一些方便的新功能-最重要的是, 异步/等待。

Node 8中的异步/等待

回调和Promise通常用于处理JavaScript中的异步代码。回调因生成无法维护的代码而臭名昭著。它们引起了JavaScript社区的混乱(特别是称为回调地狱)。 Promises将我们从回调地狱中解救了很长时间, 但是他们仍然缺乏同步代码的整洁度。异步/等待是一种现代方法, 可让你编写看起来像同步代码的异步代码。

尽管异步/等待可以在以前的Node版本中使用, 但它需要外部库和工具, 例如, 通过Babel进行额外的预处理。现在就可以直接使用。

在某些情况下, 异步/等待优于传统的承诺。

有条件的

假设你正在获取数据, 然后将根据有效负载确定是否需要新的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;
        }
    });
};

如你所见, 仅从一个额外的条件来看, 上面的代码已经看起来很凌乱。异步/等待涉及较少的嵌套:

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, 你可能会得到如下代码:

const request = () => {
    return fetchUserData()
        .then(userData => {
            return fetchCompanyData(userData);
        })
        .then(companyData => {
            return fetchRetiringPlan(userData, companyData);
        })
        .then(retiringPlan => {
            const retiringPlan = retiringPlan;
        });
};

在这种情况下, 需要链接的异步调用的是异步/等待状态:

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 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回调样式。它应将回调作为最后一个参数, 即(错误, 有效载荷)=> {…}。

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回调样式函数。

销毁可读和可写函数

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不一定表示有任何问题。

点差算子

散布运算符(aka …)在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相比, 它更快, 更安全, 更高效。 Google建议你使用它。但是它还能做什么?

多路复用

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

服务器推送

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

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

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

优先次序

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

摆脱旧的不良习惯

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

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

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

但是如何使用HTTP / 2?

大多数浏览器仅通过安全的SSL连接支持HTTP / 2。本文可以帮助你配置自签名证书。将生成的.crt文件和.key文件添加到名为ssl的目录中。然后, 将以下代码添加到名为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 srcmini');
});

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达到了性能提升, 并具有异步/等待, 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
赞(0)
未经允许不得转载:srcmini » 现在该使用Node 8了吗?

评论 抢沙发

评论前必须登录!