0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

仮想 DOM とは?

Posted at

React における仮想 DOM について調べました。

DOM とは

DOM(Document Object Model)は、HTML を JavaScript から操作できる
オブジェクトの木として表したもの。

HTML はただの文字列なので、そのままでは JavaScript から
「ここに <p> がある」「この要素を変更したい」といった操作を
簡単に行うことができない。

そこでブラウザは HTML を解析し、
ノード(Node)と呼ばれるオブジェクトを作成する。
これらのノードはブラウザ内部で管理され、
親子関係でつながったツリー構造として保持される。

このツリー構造全体を DOM と呼ぶ。

<div>
  <h1>Hello</h1>
  <p>World</p>
</div>

⬇︎ ブラウザ内部では

div
 ├─ h1
 └─ p

のような感じでツリーとして扱われる。

ブラウザ内部で何が起きている?

  1. HTML を受け取る(文字列)
  2. 解析(パース)してノードを作成する
  3. ノード同士を親子関係をつないでツリーにする(DOM ツリーの完成)

ブラウザの中には「div ノード」「h1 ノード」「p ノード」みたいなオブジェクトができている。

ツリーってどういう意味?

DOM は大体以下の形の参照(ポインタ)を持っていると思うとイメージしやすい。

  • 親は子を知っている(children)
  • 子は親を知っている(parentNode)
  • 兄弟もたどれる(nextSibling/previousSibling)

だから、

  • divの子にh1pがいる
  • h1の次の兄弟がp
    みたいな構造を JS でたどれる。

「Hello」や「World」もノード

<div>
  <h1>Hello</h1>
  <p>World</p>
</div>

実は DOM 的には 「Hello」や「World」もノード。

ざっくりこうなる:

Document
└─ html
   └─ body
      └─ div
         ├─ h1
         │  └─ #text "Hello"
         └─ p
            └─ #text "World"
  • h1は Element Node
  • Helloは Text Node
  • 全体の起点はdocument(Document Node)
    なので、「DOM = タグのツリー」ではなく、「DOM = ノード(要素・テキスト・コメント等)全体のツリー」

DOM があると何ができる

DOM はオブジェクトなので、JavaScript から次のような操作ができます。

1. 取得できる

const h1 = document.querySelector("h1");

2. 中身を変えられる

h1.textContent = "Hi!";
h1.classList.add("title");

3. ノードを追加・削除できる

const li = document.createElement("li");
li.textContent = "New";
document.querySelector("ul").appendChild(li);

DOM は、ブラウザが持つ画面の構造データであり、JS はそれを操作して UI を変える

DOM と画面は同じ?

DOM = 画面そのものではない。

  • DOM は「構造(何があるか)」のデータ
  • そこからブラウザは「どう見えるか」を計算して描画する

DOM を変更すると、ブラウザ内部では:

  • レイアウト計算
  • 再描画(ペイント)
  • 合成(コンポジット)

といった処理が発生することがあり、これが DOM 操作が高コストになりやすい と言われる理由です。

Vanilla JS で DOM を扱うと何が大変か

Vanilla JS では、「状態 → UI」の一致を人間が手作業で維持する必要があります。

「状態 → UI」とは

  • 状態(state) = アプリの今の情報
  • UI = その状態を画面に見せたもの

状態が変わったら、それに合わせて DOM を正しく操作しなければならない。

例:TODO リストで追加する場合

addBtn.addEventListener("click", () => {
  todos.push(input.value); // 状態が変わる

  // UIも更新しなきゃいけない(命令)
  const li = document.createElement("li");
  li.textContent = input.value;
  list.appendChild(li);

  input.value = ""; // UI更新(命令)
  emptyMsg.style.display = "none"; // UI更新(命令)
});

怖いポイントは、

  • 空表示を消し忘れる
  • input をクリアし忘れる
  • リスト更新だけされて状態と UI がずれる
  • 「削除」「フィルタ」「並び替え」も増えると、分岐が爆発的に増える。

この「状態と UI の一致を人間が手作業で維持」する必要をなくしたのが仮想 DOM

仮想 DOM とは

仮想 DOM とは、「今の state なら UI はこうあるべき」という構造を表した JavaScript オブジェクトのツリー。
React では直接 DOM を触らずに、

  1. state を変える
  2. React がもう一度render(=UI の計算)する
  3. 変わったところだけ DOM に反映する

という流れ UI を更新する。

React は「状態を変える」だけでいい

setTodos([...todos, text]);
setText("");
  • DOM 操作を書かない
  • 表示・非表示を命令しない
  • 「どう見せたいか」ではなく「どういう状態か」だけを書く

これが **「状態 → UI を自動で一致させる」**という意味。

例:TODO リストで追加する場合

function App() {
  const [todos, setTodos] = useState<string[]>([]);
  const [text, setText] = useState("");

  const add = () => {
    setTodos([...todos, text]); // 状態を更新
    setText("");                // 状態を更新
  };

  return (
    <>
      <h1>TODO</h1>

      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={add}>追加</button>

      {todos.length === 0 ? (
        <p>まだありません</p>
      ) : (
        <ul>
          {todos.map((t) => <li key={t.id}>{t}</li>)}
        </ul>
      )}
    </>
  );
}

ここでのポイントは:

  • liを作って append していない
  • empty 表示の切り替えも命令していない
  • input をクリアするのも DOM 命令じゃない

「状態をこうしたい」だけを書いたら、UI が勝手に一致する
→ これが、「状態 → UI を自動で一致」の意味。

Vanilla JS で TODO リストに機能を増やすと

先ほどの例の TODO リストに「削除」「完了チェック」「フィルタ」を入れると、「状態の組み合わせ × UI 操作命令」を人間が全部管理することになるため、
とても大変。

状態が増える

todos = [
  { id: 1, text: "A", done: false },
  { id: 2, text: "B", done: true },
];

filter = "all" | "active" | "done";

ここまでは問題ない。

削除処理

Vanilla JS だと最低でも以下をやる必要がある。

deleteBtn.addEventListener("click", () => {
  // ① 状態を更新
  todos = todos.filter((t) => t.id !== id);

  // ② DOMからliを削除
  li.remove();

  // ③ 空メッセージ表示切替
  if (todos.length === 0) {
    emptyMsg.style.display = "block";
  }

  // ④ フィルタ中なら再判定
  applyFilter();
});
  • 「状態更新」と「DOM 操作」が分離している
  • どれか一つ忘れると UI が壊れる

完了チェック

checkbox.addEventListener("change", () => {
  todo.done = checkbox.checked;

  // 見た目変更
  li.classList.toggle("done");

  // フィルタが active のとき
  if (filter === "active" && todo.done) {
    li.style.display = "none";
  }

  // フィルタが done のとき
  if (filter === "done" && !todo.done) {
    li.style.display = "none";
  }
});
  • filter 状態を毎回考慮
  • 「今表示すべきか?」を命令で書く
  • チェック ON/OFF の両方を考える必要がある

フィルタ処理

filterBtns.forEach((btn) => {
  btn.addEventListener("click", () => {
    filter = btn.dataset.filter;

    todos.forEach((todo) => {
      const li = findLi(todo.id);

      if (filter === "all") {
        li.style.display = "block";
      } else if (filter === "active") {
        li.style.display = todo.done ? "none" : "block";
      } else {
        li.style.display = todo.done ? "block" : "none";
      }
    });
  });
});
  • DOM とデータの紐づけ管理
  • 表示/非表示をすべて命令
  • 削除・完了・完了の影響をすべて考慮

React で TODO リストに機能を増やすと

const visibleTodos = todos.filter((t) => {
  if (filter === "active") return !t.done;
  if (filter === "done") return t.done;
  return true;
});

return (
  <ul>
    {visibleTodos.map((t) => (
      <li key={t.id}>
        <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
        {t.text}
        <button onClick={() => remove(t.id)}>削除</button>
      </li>
    ))}
  </ul>
);
  • 「表示/非表示」を命令していない
  • フィルタは計算結果
  • UI は state の写像
    → 状態が正しければ UI も必ず正しい

仮想 DOM の価値は「速さ」ではない

仮想 DOM の最大の価値は「速さ」ではなく、人間が安全に UI を書けること。

速度だけで見ると仮想 DOM は必須ではない

  • ブラウザはもともと差分描画をする
  • Vanilla JS でも正しく書けば最小限の再描画

仮想 DOM が活きるときと、本当の仕事

人間が DOM 差分を管理するのが破綻するような

  • DOM 数:数千~数万
  • 状態が複雑に絡む
  • 条件分岐・非同期更新が多い
    の時に、仮想 DOM が効く

で、仮想 DOM の本当の仕事は、

1. DOM 操作の抽象化

// React
{
  isLoggedIn && <Profile />;
}

react では上記のように記載するが、実際には

  • 追加
  • 削除
  • 並び替え
  • イベントを維持
    これらを全部 React 側が担当する

2. 安全な全体再計算

React は「今の state なら UI はこうあるべき」ということを考えてくれるため、人間は「どこを消す?」「input は残す?」「イベントは?」を考えなくていい

3. バグを防ぐ

  • 消し忘れ
  • 二重追加
  • 状態ずれ
    といったものを防ぐ

仮想 DOM は遅い?

仮想 DOM 生成、差分比較の処理があるため、JS の計算コストは確実にある
けれど、DOM の操作や人的ミスのコストはより高い。

まとめ

  • DOM は、ブラウザが HTML を解析して作る オブジェクトのツリー
  • DOM は JavaScript の普通のオブジェクトではなく、ブラウザが管理する構造データ
  • Vanilla JS では「状態 → UI」の一致を人間が手作業で維持する必要がある
  • 仮想 DOM は「今の state なら UI はこうあるべき」という構造を表した JavaScript オブジェクトのツリー
  • React は仮想 DOM を使って UI を再計算し、差分だけを実 DOM に反映する
  • 仮想 DOM の最大の価値は「速さ」ではなく、人間が安全に UI を書けること

仮想 DOM によって、
「どの DOM を追加する?消す?更新する?」という命令を書く必要がなくなり、
「状態をどうしたいか」だけを考えればよくなる。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?