仮想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)