ゼロから仮装DOMを作りましょう(パート1)
自分の簡単な仮装DOM(Virtual DOM)を作ってみました。ソースはここです。
環境と設定
JSXを使います。参照:https://babeljs.io/docs/plugins/transform-react-jsx/
JSXとは?
JSXはXML/HTMLのような言語です。例えば:
let
foo = <div id="foo">Hello!</div>
をJSXに変えると、 var
foo = h('div', {id:"foo"}, 'Hello!')
になります。h()
は、自分で定義する関数です。babel
でコンパイルします。
yarn add babel-plugin-transform-react-jsx babel-cli
を実行します。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="root"></div>
<script src="vdom.js"></script>
<script>
const root = document.getElementById('root')
render(root)
</script>
</body>
</html>
.babelrc
{
"plugins": [
"transform-react-jsx"
]
}
package.json
{
"scripts": {
"compile": "babel index.js --out-file vdom.js"
},
"dependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.24.1"
}
}
1. HTMLをJSXに変える
index.js
を作成します。
function view(count) {
return(
<ul id="cool" className="cool">
<li className="tester">
Line 1
</li>
<li className="tester">
Line 2
</li>
</ul>
)
}
これはJSXに変えたいHTMLです。次に、JSXの変化に使う関数を作ります。Reactだったら、React.createElement()
という関数を使います。私はh()
という関数を使います。
function h(type, props, ...children) {
props = props || {}
return { type, props, children }
}
この関数をJSXの変化に使うため、ファイルの上の方にに定義します。
/**
* @jsx h
*/
そして、yarn compile
を実行すると、view()
はJSXに変わります。
function view(count) {
return h(
"ul",
{ id: "cool", className: "cool" },
h(
"li",
{ className: "tester" },
"Line 1"
),
h(
"li",
{ className: "tester" },
"Line 2"
)
);
}
2. 仮装DOMを本当のDOMにレンダーする
次に、JSXをDOMにレンダーします。二つの新しい関数を定義します。render()
とcreateElement()
。
function render(el) {
el.appendChild(createElement(view(0)))
}
el
は、index.html
の<div id=”root”></div>
です。次は createElement()
です。createElement()
で要素をDOMにレンダーする関数です。
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node)
}
const el = document.createElement(node.type)
return el
}
createTextNode
は<li>テキスト</li>
の中のテキストをレンダーするに使います。createElement
は<li>
, <div>
に使います。またyarn compile
を実行して、index.html
をブラウザーで見てみると・・・まだなにも表示されていないですが、DOMを観察すると
<div id="root">
<ul></ul>
</div>
があるはずです!createElement
を再帰でJSXを全部ランダーできます。createElement
を更新しましょう。
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
}
またコンパイルすると、
- Line 1
- Line 2
が表示されます。
3. 要素に属性を追加
view
のHTMLでid
、className
を書きましたが、まだ追加していません。追加しましょう。まずはcreateElement
を更新します。
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node)
}
const el = document.createElement(node.type)
setProps(el, node.props) // ここに追加します。
node.children
.map(createElement)
.forEach(el.appendChild.bind(el))
return el
}
そして、setProps
とsetProp
という関数を作ります。setProps
で要素の属性に対して一度一つずつsetProp
を呼び出して、setAttribute()
で追加します。className
は例外で、class
になります。
function setProps(target, props) {
Object.keys(props).forEach(name => {
setProp(target, name, props[name]
})
}
function setProp(target, name, value) {
if (name === 'className') {
return target.setAttribute('class', value)
}
target.setAttribute(name, value)
}
コンパイルすると、属性は追加されるはずです。
4. diffアルゴリズム
今のところ、仮装DOMを作りますた。仮装DOMの実装には、二つの点があります。一つ目は、仮装DOMの構造体表現です。それは今までできたことです。二つ目は、仮装DOMの現在のステートと、次のステートとのdiff/patchアルゴリズムです。
仮装DOMと本当のDOMを比較するとき、四つの可能性があります。
1. UPDATE (更新)
node.type
があれば、あるいは要素はテキストではなければ、UPDATEを呼び出して、children
の中で違い があるのかを確認します。
2. CREATE (作成)
<ul>
<li>El 1</li>
<li>El 2</li>
<ul>
と
<ul>
<li>El 1</li>
<li>El 2</li>
<li>El 3</li> <!-- これを作成します
<ul>
3. REPLACE (取って代わる)
childrenが変わったら、取って代わります。以下の例をみてください。
<ul>
<li>El 1</li>
<li>El 2</li>
<ul>
と
<ul>
<li>El 1</li>
<li>El 5</li>
<ul>
上の場合では、<li>
を取って代わります。流れはこうです:
<ul>
: UPDATE
<li>
(El 1): 前と今のステートに変わりがないので、何もしません。
<li>
(El 2) -> (El 5): 違いがあるので、<li>El 2</li>
を<li>El 5</li>
に取って代わります。
今のところで、view()
で作ったマークアップがいつも同じですね。setInterval()
で更新します。それから、UPDATE, CREATE, REPLACE, REMOVEの機能を開発します。(次の記事!)