echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Node.js】Node.js version16(ES2021)の新機能

はじめに

2021年4月に、Node.jsのversion10がサポート終了となり、AWSからも「Lambdaのバージョンを更新してください」という案内が来てました。

そして、それと同じタイミングでversion16がリリースされました。
今年の10月にステータスがActiveになりますので、そうなったらLambdaでもサポートされると思われます。

そこで、Node.js version16(=ES2021)の新機能について、確認してみました。

version16の新機能一覧

  • replaceAll
  • Promise.any
  • WeakRefs
  • Finalizers

※numeric separator, 及び演算子(operator)系は省略します。

参照ページ

replaceAll

これは、名前の通り「(文字列の)一括置換」を行うメソッドです。(直球)

今まで一括置換を行う場合、replace()関数で置換対象文字列に正規表現リテラルを指定しなければならなかったですが、replaceAll()関数を使えば、単に文字列を指定すればOKです。(下記ソース参照)

知らないと結構ハマる部分だと思うので(特に多言語を知っている人がJavaScriptを触る場合)、専用の関数ができたことで分かりやすくなったと思います。

const baseStr = 'hoge|fuga|piyo';  
  
// これだとreplacedは「hoge_fuga|piyo」になる。(最初の一致文字列しか置換しない)  
const replaced = baseStr.replace('|', '_');   
  
// replace関数の場合、一括置換は正規表現リテラルを指定しないといけない。  
const replacedGlobal = baseStr.replace(/|/g, '_');  
  
// replaceAll関数なら、正規表現リテラルを指定しなくても一括置換してくれる。    
const replacedAll = baseStr.replaceAll('|', '_');  

Promise.any

version12のPromise.allSettledに続き、Promiseの機能追加です。

これは「引数に指定した複数のPromiseのうち、いずれか1つでもresolveされたら、そのうち最初にresolveされたPromiseを返す」というものです。
逆に全Promiseがrejectされた場合は、「AggregateError」というエラーをthrowします。(≒rejectされる)

Promise.raceと似ていますが、Promise.raceとの違いは「いずれか1つでもresolveされたPromiseがあったら、エラーにならない」という点です。(Promise.raceは、最初に結果が返ってきたPromiseがrejectならその時点でエラーになる)

なので例えば下記ソースのexecute関数を実行した場合、

  • Promise.any:「promise3 is resolved」とINFOログが表示される。(=正常終了)
  • Promise.race:「promise1 is rejected」とERRORログが表示される(=エラー発生)

という違いがあります。

const execute = async () => {
    try {
        // ここでPromise.anyかPromise.raceのいずれかを実行する。  
        // const promise = await Promise.any([promise1(), promise2(), promise3()]);  
        // const promise = await Promise.race([promise1(), promise2(), promise3()]);
        console.info(promise);
    } catch (e) {
        console.error(`error message is ${e.message}`);
    }
};

// 指定ms間処理をwaitさせる処理(多言語のsleepみたいなもの)
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  
// 1秒待ってエラーをthrow(=reject)
async function promise1() {
    await _sleep(1000);
    throw new Error("promise1 is rejected");
}
  
// 2秒待ってエラーをthrow(=reject)
async function promise2() {
    await _sleep(2000);
    throw new Error("promise2 is rejected");
}
  
// 3秒待って正常終了(=resolve)
async function promise3() {
    await _sleep(3000);
    return "promise3 is resolved";
}  

ちなみに、Promise.xxx系の各メソッドの挙動は下記のとおりです。

関数 正常終了する条件 エラーになる条件 正常終了した場合の戻り値
Promise.race 最初に結果が返ってきたPromiseがresolveされた 最初に結果が返ってきたPromiseがrejectされた 最初に結果が返ってきたPromise
Promise.all 全Promiseがresolveされた 1つでもrejectされたPromiseがある 全Promise(配列)
Promise.allSettled 必ず正常終了する なし(エラーになることはない) 全Promiseのstatus、およびvalue(resolve時)またはreason(reject時)(配列)
Promise.any 1つでもresolveされたPromiseがある 全Promiseがrejectされた 最初にresolveされたPromise

WeakRefs

今回の新機能で、一番分かりにくい部分だと思います。

直訳すれば「弱い参照」、MSNのサイトでは、下記の通りに記載されています。

A WeakRef object lets you hold a weak reference to another object, without preventing that object from getting garbage-collected.

訳:WeakRefオブジェクトは、ガベージコレクションを妨げることのない他のオブジェクトへの弱い参照を保持します。

とありますが、概要としてはこんな感じです。

  • WeakRefsを使用して、あるオブジェクトを参照できる。(値の取得など)
  • 「あるオブジェクト」は、WeakRefs以外から一切参照されなくなったら、ガベージコレクションの対象になる。(=破棄される)
  • 「あるオブジェクト」がガベージコレクションされたら、WeakRefsからも参照不可になる。

なおWeakRefsを使用するケースとしては「サイズが大きいオブジェクトのキャッシュやマッピング」です。

ソースで説明すると、下記の様な感じになります

  1. obj.valueにオブジェクトを定義。
  2. WeakRefインスタンスを作成し、参照するオブジェクトにobj.valueを設定し、その内容をログ出力。
  3. obj.valueをnullにし、WeakRef以外からの参照を無くし(=ガベージコレクションの対象にする)、その直後のWeakRefの弱参照オブジェクトの内容を出力
  4. 1秒ごとに、WeakRefの弱参照オブジェクトの内容を出力。(どこかでobj.valueガベージコレクションされる)
let logs = "";  
  
// ログの記載(これは毎回使う)
const log = (...args) => {
    log += args.map(a => JSON.stringify(a)).join("  ") + "\n"
    console.log(logs);
}
  
// 1
const obj = {
    value: { hoge: 'fuga'}
}
  
// 2  
// WeakRefの引数は、オブジェクトじゃないとダメ。(1とか'a'とかはNG)  
// WeakRef.deref()メソッドで、弱参照しているオブジェクトを取得できる
const ref = new WeakRef(obj.value)
log(ref.deref())
  
// 3
obj.value = null
log(ref.deref())
  
// 4
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、結果はこうなりました。
f:id:Makky12:20210605074840p:plain

obj.valueをnullにしてから17秒後にガベージコレクションにより、ref.deref()の値が参照できなくなりました。

とはいえ、いつ参照できなくなるかはガベージコレクション次第なので、当然結果は毎回違います。
なので、MDNを始めいろんなサイトに書かれている通り、WeakRefは「できる限り使用しない」のが良いと思います。(使用するとしたら「異常にメモリを使用しているソースで、何がメモリを大量に使用しているか調べる」場合でしょうか?)

Finalizer

Finalizerは先程のWeakRefsに関連する機能で「WeakRefが弱参照しているオブジェクトがガベージコレクションにより参照不可になったこと」をトリガにして起動するコールバック関数を提供する機能です。
FinalizationRegistryクラス、およびそのregisterメソッドを使用して、Finalizerを実装します。

詳細はソースを見た方が早いと思いますので、下記ソースを参照。(WeakRefsのソースにFinalizerの実装を追加してます)

const log = (...args) => {
    document.getElementById("weakRefs").innerText += args.map(a => JSON.stringify(a)).join("  ") + "\n"
}
  
const obj = {
    value: { hoge: 'fuga'}
}
  
const ref = new WeakRef(obj.value);
  
// ここからがFinalizer実装部分。  
// FinalizationRegistryインスタンスを生成する。  
// コンストラクタ引数に、コールバック関数を定義する。  
// valueには、register()メソッドの第2引数が代入される。
const registry = new FinalizationRegistry(value => {
    log(`${value} は参照不可になりました。`);
});  
  
// FinalizationRegistry.register()メソッドにて対象のオブジェクト、  
// およびコールバック関数に渡す引数を設定する。  
// ここの第二引数(今回は文字列「obj.value.hoge」)が、コールバック関数のvalueに入る。
registry.register(obj.value, "obj.value.hoge");
  
log(ref.deref())
  
obj.value = null
log(ref.deref())
  
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、上記ソースの実行結果はこちら。(ちなみにWeakRefの時と、ガベージコレクションのタイミングが違っていることがわかると思います。このことからもWeakRefの挙動はガベージコレクション次第だというのが分かると思います。)
f:id:Makky12:20210605080605p:plain

確かにobj.valueガベージコレクションにより参照不可になった時点でFinalizerのコールバック関数が起動し、「obj.value.hogeは参照不可になりました」というログが出力されています。

ただ、WeakRefsが「できる限り使用しないのが良い」ので、Finalizerも出番は少ないかもしれませんね。(出番があれとすれば、やはり調査系でしょうか)

まとめ

一部省略しましたが、Node.js verison16(ES2021)の新機能をざっと紹介しました。

個人的に劇的に大きな変更というのはあまり無い感じですが、replaceAll, Promise.anyなんかは使いどころがありそうだなあ、と思いました。(特に前者)

便利な機能はどんどん実際のプロダクトにも導入して、ソース実装を便利にしていきたいですね。

告知

6/28(月) 19:00開催の「Node学園 36時限目 オンライン」というイベントで「node10→12/14で便利になった点」という内容で登壇することになりました。

内容は主にLambda実装の観点で「node version10→version12以降で役に立った機能」について紹介する予定です。
あと「node10→12/14」とありますが、時間があれば一部version16の機能も紹介する予定です。

nodejs.connpass.com

それでは、今回はこの辺で。