hyperHTMLやlit-htmlが効率的に再描画を行う仕組み

昨今のJavaScriptのViewライブラリは以下の特徴を備えています。

- DOM構造を宣言的に記述できる
- 動的な値のマッピングを宣言的に記述できる
- **再描画の際、DOM構造の変異を最小限に留める**

例えば、[React](https://reactjs.org/)であればJSXと仮想DOMによるdiff/patch処理によってこれらを実現しています。

一方、[hyperHTML](https://github.com/WebReflection/hyperHTML)（および[lit-html](https://github.com/Polymer/lit-html)）はECMAScriptの構文であるTagged Template Literalを使ってこれらの機能を実現するライブラリです。

```javascript
import hyper from 'hyperhtml/esm';

const render = () => {
  hyper(document.getElementById('container'))`
    <h1>Hello</h1>
    <p>It is ${new Date().toLocaleString()}</p>
  `;
};

render();
setInterval(render, 1000); // 日時を表示するテキストノードのみ変異する
```

```javascript
import { html, render } from 'lit-html/lib/lit-extended';

const doRender = () => {
  render(
    html`
      <h1>Hello</h1>
      <p>It is ${new Date().toLocaleString()}</p>
    `,
    document.getElementById('container')
  );
};

doRender();
setInterval(doRender, 1000); // 日時を表示するテキストノードのみ変異する
```

「宣言的な記述」「動的な値の挿入」はまさにTemplate Literalの得意とするところです。そして、「効率的な再描画」はTagged Template Literalの仕様を活かして、比較的シンプルに実装されています。この記事では、これらのライブラリがどのようにして「効率的な再描画」を実現しているのかを詳解します。

## Template Tag関数によってどこが動的な部分なのかが分かる

Tagged Template LiteralのTagにあたる部分は、ただの関数です。第一引数にリテラル部分の文字列の配列を、第二引数以降に`${...}`による挿入部分の値を受け取ります。

```javascript
const tag = (template, ...values) => ({ template, values });

const user = {
  name: 'rikuba',
  country: 'JP'
};

tag`
  <div class="user">
    <i class="${`user-country flag-icon flag-icon-${user.country}`}"></i>
    <span class="user-name">${user.name}</span>
  </div>
`;

戻り値 = {
  "template": [
    "\n  <div class=\"user\">\n    <i class=\"",
    "\"></i>\n    <span class=\"user-name\">",
    "</span>\n  </div>\n"
  ],
  "values": [
    "user-country flag-icon flag-icon-JP",
    "rikuba"
  ]
};
```

第一引数`template`の配列を`<!--ランダム文字列-->`でjoinし、&lt;template&gt;要素のinnerHTMLに代入すれば、以下のようなDOMツリーが得られます。

```html
<div class="user">
  <i class="<!--ランダム文字列-->"></i>
  <span class="user-name"><!--ランダム文字列--></span>
</div>
```

このDOMツリーを順に辿っていけば、動的な値（タグ関数の第二引数以降の値 `...values`）をどこにマッピングすればよいかという情報を集めることができます。

- 要素ノードにおいて、属性値が`<!--ランダム文字列-->`であれば、その属性値に値がマッピングされる
- コメントノードにおいて、その内容が`ランダム文字列`であれば、その位置に値がマッピングされる（Nodeが動的に挿入される）

この情報から、次のような処理を行う関数を作ることができます。

- `values[0]`をルートの`div`から1番目の子要素のclass属性値として代入する関数
- `values[1]`をルートの`div`から2番目の子要素の最初の子要素として挿入する関数

これらの関数を順に適用することで、この構造のDOMツリーに対して効率的に値を適用することができます。ちなみに、これらの関数はhyperHTMLでは[Update](https://github.com/WebReflection/hyperHTML/blob/edbea4f859ab8e0d3e89a0f8bb5c348f6d804c0c/esm/objects/Updates.js#L53)、lit-htmlでは[Part](https://github.com/Polymer/lit-html/blob/54afddcfa7de802181e502f3b69599aa0507efb8/src/lit-html.ts#L431)に当たります。

この「文字列配列からDOMツリーを作り、順に辿って関数を作っていく」処理は、1つのテンプレート文字列に対しては1回実行すれば十分です。そこでTagged Template Literalのもう一つの言語仕様が使えます。

## Template ObjectはJSエンジンによってキャッシュされている

タグ関数の第一引数には、同じテンプレート文字列に対しては常に**同一**の配列オブジェクト（正確にはTemplate Object）が渡されます。リテラル部分の文字列が同じであれば、`${...}`の値は関係ありません。

```javascript
// Template Objectをそのまま返すだけのタグ関数
const tag = (template, ...values) => template;

tag`私は${'rikuba'}です。` === tag`私は${'りくば'}です。`; //=> true (リテラル部分が同じ)
tag`私は${'rikuba'}です。` === tag`わたしは${'りくば'}です。`; //=> false (リテラル部分が異なる)
```

この仕様により、呼び出される側のタグ関数内では、同じ箇所のTagged Template Literalからの呼び出しであることを自然に検知できます。`Map`などを使うことで、2回目以降は処理を省略できます。

```javascript
const map = new WeakMap;
const tag = (template, ...values) => {
  // 初めて見るテンプレートであれば、`parse`処理を行う
  if (!map.has(template)) {
    const result = parse(template);
    map.set(template, result);
  }

  const { element, patches } = map.get(template);
  patches.forEach((patch) => patch(element, values));
  return element;
};

const parse = (template) => {
  const patches = [];
  // ...前節で説明したような処理
  return {
    get element() { return element.cloneNode(true); },
    patches
  };
};

const paragraph = (message) => tag`<p>${message}</p>`;
const link = (text, url) => tag`<a href="${url}">${text}</a>`;

const p1 = paragraph('Hello');
const a = link('Qiita', 'https://qiita.com/');
const p2 = paragraph('World');  // `parse`は実行されない
```

この仮想コードではtagが毎回elementを作って返していますが、elementを引数として受け取るようにすれば、その要素に対するpatch処理になります。hyperHTMLであれば`bind`が、lit-htmlであれば`render`がそれに当たります。

### 実際のところ……

「Template ObjectがJSエンジンによってキャッシュされる」という[仕様](https://tc39.github.io/ecma262/#sec-gettemplateobject)は、TypeScriptによってdownpileされたコードでは必ずしも[満たされない](https://github.com/Microsoft/TypeScript/issues/17956)ため、ライブラリ作者は注意が必要です。hyperHTMLは同じテンプレート文字列から確実に同一のTemplate Objectを得るための[処理](https://github.com/WebReflection/hyperHTML/blob/edbea4f859ab8e0d3e89a0f8bb5c348f6d804c0c/esm/shared/utils.js#L108)を通すことで対処しています。

## 結び

[hyperHTML](https://github.com/WebReflection/hyperHTML)や[lit-html](https://github.com/Polymer/lit-html)は、機能やエコシステムの面ではまだこれからではありますが、その実装はとてもスマートであり、IEを無視すればtranspileも必要ないので、使いやすく理解しやすいライブラリだと思います。

## 参考

- [hyperHTML, the nitty gritty](https://gist.github.com/WebReflection/d3aad260ac5007344a0731e797c8b1a4)
- [VDOM vs lit-html - HTTP203 - YouTube](https://www.youtube.com/watch?v=uCHZJy2n8Qs)