Что нового в Node.js 15?

Автор оригинала: Jennifer Fu
  • Перевод
Делимся переводом статьи, в которой собраны подробности о новых функциях 15-й версии Node.js.

Версия Node.js 15 была выпущена 20 октября 2020 года. Среди основных изменений:

  • режим throw при необработанных отклонениях
  • особенности языка V8 8.6
  • NPM 7
  • экспериментальная поддержка QUIC
  • N-API Version 7
  • доработка API асинхронного локального хранилища (Async Local Storage)

Давайте подробнее рассмотрим, что эти нововведения из себя представляют и как их можно использовать.

Использование NVM для обзора Node


В предыдущей статье мы разобрали инструкции по использованию NVM (Node Version Manager) для управления версиями Node.js и NPM. В нашей среде были установлены Node.js 12.16.0 и NPM 6.14.8. Запустив nvm install node, мы установили Node.js 15.4.0 и NPM7.0.15.

У нас открыто два окна, в одном Node.js 12, а в другом — Node.js 15.

В окне node12:

$ nvm use 12
Now using node v12.16.0 (npm v6.14.8)

В окне node15:

$ nvm use 15
Now using node v15.4.0 (npm v7.0.15)

Теперь мы можем исследовать эту версию.

Режим Throw при необработанном отклонении промиса (promise)


Событие unhandledRejection генерируется каждый раз, когда промис (promise) отклоняется и обработчик ошибок не прикрепляется к промису в ходе цикла обработки событий. Начиная с версии Node.js 15, режим по умолчанию для unhandledRejection был изменен с warn на throw. В режиме throw, если хук unhandledRejection не установлен, unhandledRejection генерируется как исключение, не пойманное методом catch.

Создайте программу, чтобы промис отклонялся (rejected) с сообщением об ошибке:

function myPromise() {
  new Promise((_, reject) =>
    setTimeout(
      () =>
        reject({
          error: 'The call is rejected with an error',
        }),
      1000
    )
  ).then((data) => console.log(data.data));
}

myPromise();

Когда вы запускаете этот код в окне node12, появляется длинное сообщение с предупреждением:

$ node myPromise.js
(node:79104) UnhandledPromiseRejectionWarning: #<Object>
(node:79104) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:79104) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.Users that have an unhandledRejection hook should see no change in behavior, and it’s still possible to switch modes using the --unhandled-rejections=mode process flag.

При запуске этого кода в окне node15 генерируется ошибка UnhandledPromiseRejection:

$ node myPromise.js
node:internal/process/promises:227
          triggerUncaughtException(err, true /* fromPromise */);
          ^[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "#<Object>".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

Добавьте обработчик ошибок в ветвь then в приведенном ниже коде (.catch((error) => console.log(error.error)) также работает).

function myPromise() {
  new Promise((_, reject) =>
    setTimeout(
      () =>
        reject({
          error: 'The call is rejected with an error',
        }),
      1000
    )
  ).then(
    (data) => console.log(data.data),
    (error) => console.log(error.error)
  );
}

myPromise();

Теперь код запускается корректно в обоих окнах (node12 и node15):

$ node myPromise.js
The call is rejected with an error

Обработчик ошибок для промисов рекомендуется написать. Однако возможны случаи, когда ошибки не ловятся методом catch. Рекомендуется настроить хук unhandledRejection для обнаружения потенциальных ошибок.

function myPromise() {
  new Promise((_, reject) =>
    setTimeout(
      () =>
        reject({
          error: 'The call is rejected with an error',
        }),
      1000
    )
  ).then((data) => console.log(data.data));
}

myPromise();

process.on('unhandledRejection', (reason, promise) => {
  console.log('reason is', reason);
  console.log('promise is', promise);
  // Application specific logging, throwing an error, or other logic here
});

Хук unhandledRejection работает как в Node.js 12, так и в Node.js 15. После его установки, unhandledRejection обрабатывается как нужно.

$ node myPromise.js
reason is { error: 'The call is rejected with an error' }
promise is Promise { <rejected> { error: 'The call is rejected with an error' } }

V8 8.6 Новые возможности языка


V8, движок JavaScript, обновлен с 8.4 до 8.6. версии. Помимо разных хитростей для повышения производительности, новая версия V8 обладает следующими фичами:

  • Promise.any() и AggregateError (из V8 8.5)
  • await setTimeout и AbortController (экспериментальная фича)
  • String.prototype.replaceAll() (из V8 8.5)
  • Логические операторы присваивания &&=, ||= и ??= (из V8 8.5)

Promise.any() и AggregateError


Для начала давайте рассмотрим существующий метод Promise.all().

Promise.all() принимает итерируемое из промисов в качестве входных данных и возвращает один промис, который исполняется как массив результатов входных промисов.

Следующая программа вызывает Promise.all() для двух исполненных (resolved) промисов:

function myPromise(delay) {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          data: The data from ${delay} ms delay,
        }),
      delay
    )
  );
}

async function getData() {
  try {
    const data = await Promise.all([myPromise(5000), myPromise(100)]);
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}

getData();

Promise.all() возвращает промис, который исполнится, когда все входные промисы будут исполнены (resolved), или если iterable не содержит обещаний:

$ node myPromise.js
[
  { data: 'The data from 5000 ms delay' },
  { data: 'The data from 100 ms delay' }
]

Следующая программа вызывает Promise.all() для двух отклоненных (rejected) промисов.

function myPromise(delay) {
  return new Promise((_, reject) =>
    setTimeout(
      () =>
        reject({
          error: The error from ${delay} ms delay,
        }),
      delay
    )
  );
}

async function getData() {
  try {
    const data = await Promise.all([myPromise(5000), myPromise(100)]);
    console.log(data);
  } catch (error) {
    console.log(error);
  }
}

getData();

Promise.all() немедленно делает реджект на любое отклонение входного промиса или на любую ошибку в момент исполнения с возвратом сообщения об этой ошибке:

$ node myPromise.js
{ error: 'The error from 100 ms delay' }

Promise.any() — новый метод в Node.js 15. Он противоположен методу Promise.all(). Promise.any() принимает итерируемый объект, содержащий объекты “обещаний” Promise. И, как только одно из обещаний Promise в iterable выполнится успешно, метод возвратит единственный промис со значением выполненного «обещания».

Следующая программа вызывает Promise.any() для двух исполненных (resolved) промисов:

function myPromise(delay) {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve({
          data: The error from ${delay} ms delay,
        }),
      delay
    )
  );
}

async function getData() {
  try {
    const data = await Promise.any([myPromise(5000), myPromise(100)]);
    console.log(data);
  } catch (error) {
    console.log(error);
    console.log(error.errors);
  }
}

getData();

Promise.any() возвращает первый исполненный (resolved) промис:

$ node myPromise.js
{ data: 'The error from 100 ms delay' }

Следующая программа вызывает Promise.any() для двух отклоненных (rejected) промисов:

function myPromise(delay) {
  return new Promise((_, reject) =>
    setTimeout(
      () =>
        reject({
          error: The error from ${delay} ms delay,
        }),
      delay
    )
  );
}

async function getData() {
  try {
    const data = await Promise.any([myPromise(5000), myPromise(100)]);
    console.log(data);
  } catch (error) {
    console.log(error);
    console.log(error.errors);
  }
}

getData();

Если промисы в iterable не исполняются, т.е. все данные промисы отклоняются, возвращенный промис отклоняется с AggregateError, новым подклассом Error, который группирует вместе отдельные ошибки.

$ node myPromise.js
[AggregateError: All promises were rejected]
[
  { error: 'The error from 5000 ms delay' },
  { error: 'The error from 100 ms delay' }
]

Await setTimeout и AbortController


В предыдущих примерах мы использовали setTimeout внутри вызова промиса.

SetTimeout в WindowOrWorkerGlobalScope, использует коллбэк. Однако timers/promises предоставляют промисифицированную версию setTimeout, которую можно использовать с async/await.

const { setTimeout } = require('timers/promises');

async function myPromise(delay) {
  await setTimeout(delay);
  return new Promise((resolve) => {
    resolve({
      data: The data from ${delay} ms delay,
    });
  });
}

async function getData() {
  try {
    const data = await Promise.any([myPromise(5000), myPromise(100)]);
    console.log(data);
  } catch (error) {
    console.log(error);
    console.log(error.errors);
  }
}

getData();

AbortController — это объект JavaScript, который позволяет прервать один или несколько веб-запросов по желанию. Мы привели примеры использования AbortController в другой статье про useAsync.

И await setTimeout, и AbortController являются экспериментальными фичами.

String.prototype.replaceAll()


Давайте рассмотрим существующий метод String.prototype.replace().

replace() возвращает новую строку с несколькими или всеми сопоставлениями с шаблоном, заменёнными на заменитель. Шаблон может быть строкой или регулярным выражением. Заменитель может быть строкой или функцией, вызываемой для каждого сопоставления.

Если шаблон является строкой, будет заменено только первое вхождение.

'20+1+2+3'.replace('+', '-');

Использование данного оператора даст “20–1+2+3”.

Чтобы заменить все «+» на «-», необходимо использовать регулярное выражение.

'20+1+2+3'.replace(/\+/g, '-');

Использование указанного выше оператора даст “20–1-2-3”.

Метод replaceAll() — новинка в Node.js 15. Благодаря его использованию, нам не придется использовать регулярное выражение. Этот метод возвращает новую строку со всеми сопоставлениями с шаблоном, замененными на заменитель. Шаблон может быть строкой или регулярным выражением, а заменитель может быть строкой или функцией, вызываемой для каждого сопоставления.

Благодаря методу replaceAll(), нам можно не использовать регулярное выражение для замены всех «+» на «-».

'20+1+2+3'.replaceAll('+', '-');

Выполнение данного оператора дает “20–1-2-3”.

Логические операторы присваивания &&=, ||= и ??=


Логический оператор присваивания AND (x &&= y) выполняет операцию присваивания, только если x истинно.

x &&= y эквивалентно x && (x = y), но не эквивалентно x = x && y.

let x = 0;
let y = 1;

x &&= 0; // 0
x &&= 1; // 0
y &&= 1; // 1
y &&= 0; // 0

Логический оператор присваивания OR (x ||= y) выполняет операцию присваивания, только если x ложно.

x ||= y эквивалентно x || (x = y), но не эквивалентно x = x || у.

let x = 0;
let y = 1;

x ||= 0; // 0
x ||= 1; // 1
y ||= 1; // 1
y ||= 0; // 1

Логический оператор присваивания nullish (x ??= y) выполняет операцию присваивания, только если x имеет значение NULL (null или undefined).

x ??= y эквивалентно x ?? (x = y), и не эквивалентно x = x ?? у.

let x = undefined;
let y = '';

x ??= null; // null
x ??= 'a value'; // "a value"
y ??= undefined; // ""
y ??= null; // ""

Другие изменения


Помимо режима throw при необработанном отклонении промиса и новых языковых фич V8 8.6, в Node.js 15 есть следующие изменения:

NPM 7: Много изменений, в том числе автоматическая установка смежных зависимостей, усовершенствование пакетов и yarn.lock файлов, поддержка рабочего пространства и др. Все это описано в данной статье по ссылке.

QUIC: Экспериментальная поддержка протокола транспортного уровня UDP, который является основным протоколом для HTTP / 3. QUIC включает встроенную систему безопасности с TLS 1.3, управление потоком, исправление ошибок, миграцию подключений (connection migration) и мультиплексирование.

N-API Version 7: API для создания собственных аддонов. Не зависит от среды выполнения JavaScript, лежащей в основе, и поддерживается как часть самого Node.js.

Усовершенствование API асинхронного локального хранилища (Async Local Storage): Предоставляет возможность для более сложного логирования и анализа фич для крупномасштабных приложений.

Заключение


В новой версии Node.js 15 большое количество новых функций и улучшений, в том числе и достаточно существенных.

Попробуйте новую версию и будьте готовы к обновлению проектов.

Спасибо за внимание! Надеюсь, статья была полезна для вас.
Timeweb
VDS, инфраструктура и решения для бизнеса

Комментарии 4

  • НЛО прилетело и опубликовало эту надпись здесь
      0
      x &&= y эквивалентно x && (x = y), но не эквивалентно x = x && y.
      x ||= y эквивалентно x || (x = y), но не эквивалентно x = x || y.

      Не понял, в чем разница? По таблицам истинности очень даже эквивалентно. В JS ведь нельзя переопределять операторы.


      Кстати, читать таблицу истинности в формате


      0 0
      0 1
      1 1
      1 0

      неудобно. Лучше изменить на традиционный


      0 0
      0 1
      1 0
      1 1
        +6
        Разница в том, что x = x && y всегда присваивает (переписывает) значение.
        const obj = {
            x: 5,
            set x(a) {
                console.log('here');
            }
        };
        
        obj.x &&= 4; // сеттер здесь не вызывается
        obj.x = obj.x && 4;
        
          +1
          Разница будет если x — это свойство, у которого определен сеттер с сайд-эффектами

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое