1
/
5

ゼロから仮装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でidclassNameを書きましたが、まだ追加していません。追加しましょう。まずは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
}

そして、setPropssetPropという関数を作ります。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の機能を開発します。(次の記事!)