2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

仮想DOM


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

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

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

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


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

差分検知

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

const obj1 = {
  element: "div",
  innerText: "hello",
};

const obj2 = {
  element: "div",
  innerText: "hello",
};

obj1 === obj2
//false


これを比較するためにshallowEqualという比較方法でプロパティが同じかチェックします。

仮想DOMの差分検知


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)ファイルです

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ファイルで読み込まれているファイルです。

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を返却します。この時の戻り値は以下です。
{
  tagName:'article',
  innerText:'hello'
}
  • (1.3) render関数を実行します。
  • (1.4) getElement関数でエレメントを生成します。生成したエレメントをHTMLにレンダリングします。helloと表示されます。

クリック時


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

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?