仮想DOMの差分検知を実装してみた


# 仮想 DOM

​
DOM 構造の状態を JS で保持することによって、実際の DOM にアクセスする回数を減らし、DOM 更新の際に高パフォーマンスを発揮できる。
​

```html
<article>
  <cite> ユーザー </cite>
  <p>コメント</p>
  <img src="" />
</article>
```

このような HTML があったとしたら、SPA を使う際、JS で
​

```ts
const ui = {
  element: 'article',
  children: [
    {
      element: 'cite',
      innerText: 'ユーザー',
    },
    {
      element: 'p',
      innerText: 'コメント',
    },
    {
      element: 'img',
      src: 'https://...jpg',
    },
  ],
};
```

​
のように DOM を保持します。仮想 DOM の一部しか更新していないのに、差分を含まない Element ごとすべて実際の DOM に反映させるのは効率が悪いので、新旧の仮想 DOM を見比べて、再反映の影響範囲を最小限にします。
​

# 差分検知

JS において、オブジェクト同士の等価チェックは参照が同一か見ています。
​

```ts
const obj1 = {
  element: 'div',
  innerText: 'hello',
};
const obj2 = {
  element: 'div',
  innerText: 'hello',
};
obj1 === obj2;
//false
```

​
これを比較するために[shallowEqual](https://nishinatoshiharu.com/shallow-compare-logic/)という比較方法でプロパティが同じかチェックします。
​

# 仮想 DOM の差分検知

​
html はこれだけです。参考程度に。

```html
<head>
  // 省略
  <script src="src/index.js" type="module"></script>
  ---(1.1)
</head>
<body>
  <div id="app"><button id="renderButton">button</button> ---(2.1)</div>
</body>
```

​
こちらが html が読み込んでいる JS(TS)ファイルです
​

```ts
import { h, getElement, shallowEqual } from "./vertualDom.js";
​
const getCard = ({ comment }: { comment: string }) => {
  return h("article", comment);
};
​
const render = (vEl: ReturnType<typeof h>) => {  ---(1.3)
  const appEl = document.querySelector("#app");
​
  if (!appEl) {
    return;
  }
​
  const el = getElement(vEl); ---(1.4)
​
  appEl.appendChild(el);
};
​
const app = () => {
  const card = getCard({ comment: "hello" });
  const card1 = getCard({ comment: "hello" });
​
  const renderButton = document.querySelector("#renderButton");
​
  if (!(renderButton instanceof HTMLButtonElement)) {
    return;
  }
​
  renderButton.onclick = () => { ---(2.1)
    if (!shallowEqual(card, card1)) { ---(2.2)
      render(card);
    }
  };
​
  return card;
};
​
const appEl = app(); ---(1.2)
​
if (appEl) {
  render(appEl); ---(1.3)
}
​
```

そして、最後に上の JS ファイルで読み込まれているファイルです。
​

```ts
export const h = (tagName: keyof HTMLElementTagNameMap, innerText: string) => {
  return {
    tagName,
    innerText,
  };
};
​
// オブジェクトを実際のDOMに変換する関数
export const getElement = ({ tagName, innerText }: ReturnType<typeof h>) => { ---(1.4)
  const element = document.createElement(tagName);
  element.innerText = innerText;
​
  return element;
};
​
export const shallowEqual = <T extends ReturnType<typeof h>>( ---(2.2)
  currentEl: T,
  nextEl: T
) => { ---(2.3)
  if (currentEl === nextEl) {
    return true;
  }
  if (currentEl.tagName !== nextEl.tagName) {
    return false;
  }
  if (currentEl.innerText !== nextEl.innerText) {
    return false;
  }
  return true;
};
​
```

​

### HTML 読み込み時

​

- (1.1) html が JS を読み込みます。
- (1.2) app 関数が実行されます。関数内部を無事下まで実行し card を返却します。この時の戻り値は以下です。

```ts
{
  tagName:'article',
  innerText:'hello'
}
```

- (1.3) render 関数を実行します。
- (1.4) getElement 関数でエレメントを生成します。生成したエレメントを HTML にレンダリングします。hello と表示されます。
  ​

### クリック時

​
いよいよ、差分検知します。
​

- (2.1) クリックイベントが実行されます。
- (2.2) shallowEqual 関数が実行されます。引数は 2 つとも以下のオブジェクトを渡します。

```ts
{
  tagName:'article',
  innerText:'hello'
}
```

- (2.3)　 shallowEqual 関数の内部の条件を判定します。 - if(currentEl === nextEl)
  前述したようにオブジェクトの参照先を比較しています。今回はオブジェクトの参照は違うので通りません。 - if (currentEl.tagName !== nextEl.tagName)
  同じ tagName なので通りません。 - if (currentEl.innerText !== nextEl.innerText)
  同じ innerText なので通りません。
  ​
  結果的にどこも通らず、true が返却されます。
  よって render は実行されず、終了です。
  ​

# 以上

簡単な仮想 DOM の差分検知を実装してみました。
記事の執筆を通して、JS だけで HTML がどのようにレンダリングされているのか、多少理解が深まりました。
​
参考
(https://www.youtube.com/watch?v=xVNDJjsaW-0&t=134s)