いわゆる仮想DOMをつくります。
1. 目標物
1.1 生成されるHTML
<ul class=”list”> <li>item 1</li> <li>item 2</li> </ul>
1.2 必要なオブジェクト
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] }
1.3 オブジェクトの説明
{ type: ‘…’, props: { … }, children: [ … ] }
- typeに要素
- propsにクラス等の属性
- childrenに子ノード
2. 実際のコード
2.1 h関数(DOMツリーを受け取る)
上記のオブジェクトを処理するh関数をまず作る。
function h(type, props, …children) { return { type, props, children }; }
というわけで、以下のような形でDOMツリーを書ける。
h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), );
2.2 createElemet(子要素の処理)
子ノードにあたるchildrenの処理は2パターンある。要素と文字列だ。
function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type); }
子ノードを引数nodeで受け取り、String型かどうかで処理を分岐する。要素ならcreateElement
で要素を生成し、文字列の場合はcreateTextNode
で文字列をDOMツリーに追加する。
function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; }
$マークは深い意味はない。普通の変数名と同様の処理がされる。なんか付いてる、目立ってるくらいに考えておいてほしい。
node.children
をmap
で回してcreateElement
に放り込んでいる。つまり再帰的にcreateElement
を呼び出してこれは一番最後の子ノードまで続く。子ノードを最後まで呼び出すと、順番にforEach
で子ノードとしてDOMに追加していく。
2.3 updateElement(要素の更新)
2.3.1 便利なDOM操作一覧
以下のメソッドはDOM操作をする際に多用される。 - appendChild() 子ノードの追加 - removeChild() 子ノードの削除 - replaceChild() 子ノードの置換
次はこれらのメソッドを使いながらDOMの更新について見ていく。
2.3.2 古いnode(要素)が無い場合
function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } }
更新対象となる古いノードがないばあい、新規作成するので普通に先程の関数でノードを追加する。
2.3.3 新しいnodeが無い場合
function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } }
新しい要素が無いばあいは、子要素を削除する。
2.3.4 nodeを更新する場合
ノードを更新するばあい、if文の条件が複雑になるので特別に関数を作ります。
function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type }
以下のことを確認しています
- 要素・文字列の一致していない
- 文字列でなおかつ内容が一致していない
- ノードの属性が一致していない
これらの条件のうちどれか一つでも満たせば(一致していなければ)true
が返るという仕組み。
というわけで、updateElement
に以下の処理を追加する。
else if (changed(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); }
解説していない箇所は$parent.childNodes[index]
の箇所のみなのでほかは飛ばします。この箇所は$parent
の子ノードのインデックス番号を指定しています。replaceで置換するために必要です。
2.3.5 子ノードの比較
子ノードを比較するうえでいくつかの問題がまだ未解決で残ってる。
- 要素の場合のみ比較すべき(文字列は子要素を持てない)
- 現状、直近のノードを親要素 として参照して処理している
- 子ノードは一つずつ比較するべき。たとえ
undefined
が返ってくる可能性があったとしても
というわけで、updateElement
に以下の分岐を追加すべきだ。
else if (newNode.type) { const newLength = newNode.children.length; const oldLength = oldNode.children.length; for (let i = 0; i < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } }
新旧問わずに子ノードが存在するばあい、updateElement
を再帰的に呼び出す。引数は以下の通り。
$parent
としてn番(インデックス番号)目子要素newNode
のn番目oldNode
のn番目
以上の工程を経ることによって、子ノードがそれぞれ一つづつ比較されて処理される。
まとめ
ここまでのコードをあわせたものがこちら。
/** @jsx h */ function h(type, props, ...children) { return { type, props, children }; } function createElement(node) { if (typeof node === 'string') { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; } function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type } function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); } else if (changed(newNode, oldNode)) { $parent.replaceChild( createElement(newNode), $parent.childNodes[index] ); } else if (newNode.type) { const newLength = newNode.children.length; const oldLength = oldNode.children.length; for (let i = 0; i < newLength || i < oldLength; i++) { updateElement( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); } } } const a = ( <ul> <li>item 1</li> <li>item 2</li> </ul> ); const b = ( <ul> <li>item 1</li> <li>hello!</li> </ul> ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => { updateElement($root, b, a); });
<button id="reload">RELOAD</button> <div id="root"></div>
気になったら(CodePen.io)https://codepen.io/で確かめてみてね〜〜♪
今回は基礎てきなところのみで、いくつかの機能は未実装のまま。以下の機能が本来は実装されるべき。
しかし、これでVirtual DOMの基本のきは抑えたんだぜ、やったな🔥🔥
P.S.
JSXのところを大幅に飛ばしてしまってるけど、そんなに難しくないので大丈夫!それにこのあとJSXの記事書くし!