LoginSignup
27
22

More than 5 years have passed since last update.

Hyperappを読む

Last updated at Posted at 2017-12-30

読んだのでまとめる.

Hyperapp とは

2018 年は Hyperapp の年だ を読むとわかる.

読んでみる

読みやすさのために少し書き換えている.実際のコードは GitHub - hyperapp/hyperapp にある.

h 関数

h 関数は与えられた引数から Hyperapp の仮想 DOM で使用するノードを生成する.引数に関数を渡すとその関数を実行するようになっているので GitHub - hyperapp/html の関数を渡しても同じようにノードを生成することができる.

// { name: 'div', props: {}, children: [] }

h( 'div' )         
h( 'div', {} )
h( 'div', {}, [] )
// { name: 'div', props: {}, children: [] }

import { div } from '@hyperapp/html'

h( div )
h( div, {} )
h( div, {}, [] )

div()
div( {} )
div( [] )
div( {}, [] )

app 関数

app 関数は stateactionsviewcontainer を引数として受け取る.containerdocument.body などの要素のこと.

この関数内にある変数の rootpreactrender 関数の引数である replaceNode と同じ役割を担っている(と教えてもらいました).

この replaceNode は preact の API Reference で以下のように説明してある.

If the optional replaceNode DOM node is provided and is a child of containerNode, Preact will update or replace that element using its diffing algorithm.

const existingNode = container.querySelector('h1');

render(MyComponent, container, existingNode);
// Diff MyComponent against <h1>My App</h1>
//
// <div id="container">
//   <MyComponent />
// </div>

Hyperapp では replaceNode をこちらから指定することができない.app 関数を呼び出した際に container.children[0]replaceNode として使用される.

var patchLock
var lifecycle = []
var root = container && container.children[0]
var node = vnode(root, [].map)

state = copy(state)
actions = copy(actions)

init([], state, actions)
repaint()

return actions

repaint 関数では,render 関数を呼び出しているが setTimeout によって非同期で呼び出すようになっている.patchLock はここで使用されており,同時にいくつもの render 関数を実行しないようになっている.

function repaint(){
  if(!patchLock){
    patchLock = !patchLock
    setTimeout(render)
  }
}

function render(){
  patchLock = !patchLock
  ...
}

lifecycle はコールバック関数を配列で持っている.ノードの oncreateonremove などのプロパティが関数のとき,それらを後に出てくる render 関数内で取り出して実行する.

init 関数

init 関数は actions の関数をラップする.また,もし actions が入れ子になっていて state は入れ子になっていないときは stateactions の形に合うように拡張する.

// init

if( actions[key] === "function" ){
  (function(key, action) {
    actions[key] = function(data) {
      // action に対応した state を参照する
      slice = get(path, state)

      if (typeof (data = action(data)) === "function") {
        data = data(slice, actions)
      }

      if (data && data !== slice && !data.then) {
        // action に対応した state に値を入れる
        state = set(path, copy(slice, data), state, {})
        repaint()
      }

      return data
    }
  })(key, actions[key])
} else {
  // state を action の形に合わせる
  slice[key] = slice[key] || {}
  actions[key] = copy(actions[key])
  init(path.concat(key), slice[key], actions[key])
}

actions が入れ子になっていれば state も同じように入れ子になる.

// Action

{
  count: {
    up: value => ( state, actions ) => ( { count: state.count + value } )
  }
}
// State

{
  count: {}
}

このとき,例えば actions.count.up を呼び出すと state.countactions.count.up の引数に渡される.actions.count.up の返り値も state.count で保持される.

render 関数

render 関数は app 関数に引数として渡された view 関数を使い stateactions からノードを生成する.その後 patch 関数に生成したノードを渡す.最後に lifecycle の中に関数があれば実行する.

function repaint() {
  if (!patchLock) {
    patchLock = !patchLock
    setTimeout(render)
  }
}

function render(next) {
  patchLock = !patchLock
  next = view(state, actions)

  if (container && !patchLock) {
    root = patch(container, root, node, (node = next))
  }

  while ((next = lifecycle.pop())) next()
}

patch 関数

render 関数から渡されたノードを元に要素を生成する.

patch 関数では createElementsetElementPropupdateElementremoveElement などの関数を使用して要素の生成,更新を行なっている.これは最後の方にまとめる.

function patch(parent, element, oldNode, node, isSVG, nextSibling) {
  if (node === oldNode) {
    // node と oldNode は一致するので変更はない
  } else if (null == oldNode) {
        // oldNode が存在しないので新しい node を使用する
    element = parent.insertBefore(createElement(node, isSVG), element)
  } else if (node.name && node.name === oldNode.name) {
    // ...
  } else if (node.name === oldNode.name) {
    // oldNode と node のタグ名が一致しているので値のみ更新する
    element.nodeValue = node
  } else {
    // 要素を生成して挿入し,古い要素を削除する
        nextSibling = element
    element = parent.insertBefore(createElement(node, isSVG), nextSibling)
    removeElement(parent, nextSibling, oldNode)
  }
  return element
}

patch 関数の node.name && node.name === oldNode.name の条件部分だけを別にまとめる.

ノードのプロパティにユニークな key を指定すると,次の patch 関数の実行の際にその要素を使いまわしてくれる.key が指定されていない要素は条件によっては再生成されてしまうので,使いまわしたい場合は key を指定して再生成を防ぐ.hyperapp/docs/concepts/keys.md を見るとわかる.

By setting the key property on a virtual node, you declare that the node should correspond to a particular DOM element. This allow us to re-order the element into its new position, if the position changed, rather than risk destroying it.

var oldElements = []
var oldKeyed = {}
var newKeyed = {}

// key から oldElements[i] (要素)と oldChild (ノード)を参照できるように oldKeyed に入れる
for (var i = 0; i < oldNode.children.length; i++) {
  oldElements[i] = element.childNodes[i]

  var oldChild = oldNode.children[i]
  var oldKey = getKey(oldChild)

  if (null != oldKey) {
    oldKeyed[oldKey] = [oldElements[i], oldChild]
  }
}

var i = 0
var j = 0

while (j < node.children.length) {
  var oldChild = oldNode.children[i]
  var newChild = node.children[j]

  var oldKey = getKey(oldChild)
  var newKey = getKey(newChild)

  if (newKeyed[oldKey]) {
    i++
    continue
  }

  if (null == newKey) {
    if (null == oldKey) {
      patch(element, oldElements[i], oldChild, newChild, isSVG)
      j++
    }
    i++
  } else {
    var recyledNode = oldKeyed[newKey] || []

    if (oldKey === newKey) {
            // patch 内の updateElement で挿入された要素のプロパティを recyledNode[1] と newChild を使って更新する
      patch(element, recyledNode[0], recyledNode[1], newChild, isSVG)
      i++
    } else if (recyledNode[0]) {
      // recyledNode[0] を oldElements[i] の前に移動する
      // patch 内の updateElement で挿入された要素のプロパティを recyledNode[1] と newChild を使って更新する
      patch(element, element.insertBefore(recyledNode[0], oldElements[i]), recyledNode[1], newChild, isSVG)
    } else {
      patch(element, oldElements[i], null, newChild, isSVG)
    }

    j++
    newKeyed[newKey] = newChild
  }
}

createElement 関数

ノードから要素を生成する.

setElementProp 関数

引数として elementnamevalueoldValue を受け取って elementname プロパティに value を設定する.namestyle だったときは oldValuevalue を合わせてから設定する.

updateElement 関数

引数として elementoldPropsprops を受け取る.oldPropsprops でプロパティの値が異なっていれば setElementProp で更新する.プロパティの名前が value または checked の場合は,element のプロパティを,それ以外は以前のノードのプロパティを参照する.

if( props[name] !== ( 'value' === name || 'checked' === name ? element[name] : oldProps[name] ) ){
  setElementProp(element, name, props[name], oldProps[name])
}

removeElement 関数

要素の削除を行う.ノードのプロパティに onremove があるときには,削除する elementdone を引数として渡す.done は関数で,実行することで element の削除が行われる.

key について

key の扱いについて少しまとめる.key は patch 関数によって要素を使い回すために使用される.以下は canvas に落書きをするためのコードで,canvas の他にパレット,パレットの中には色の変更を行うためのボタンを配置し,さらにパレットの表示,非表示を切り替えるためのボタンを配置している.

このコードは calmery/drawing.html で見ることができる.

//View

const colors = {
  black: '#000',
  red: '#F00',
  green: '#0F0',
  blue: '#00F'
}

const createPalette = ( actions, colors ) =>
  h( 'div', {}, Object.keys( colors ).map( color =>
    h( 'button', { onclick: _ => actions.setColor( colors[color] ) }, color.charAt( 0 ).toUpperCase() + color.slice( 1 ) )
  ) )

const createCanvas = actions =>
  h( 'canvas', {
    width: 700,
    height: 400,
    oncreate: actions.setContext,
    onremove: ( element, done ) => {
      actions.unsetContext()
      done()
    },
  } )

const createPaletteToggle = ( state, actions ) =>
  h( 'button', {
    onclick: _ => actions.setIsShow( !state.isShow ),
    onremove: ( element, callback ) => {
      console.log( callback )
      console.log( 'asd' )
    }
  }, ( state.isShow ? 'Hide' : 'Show' ) + 'ToolBar' )

const view = ( state, actions ) =>
  h( 'div'
   , {}
   , [ createPaletteToggle( state, actions )
     , h( 'br' )
     ].concat( state.isShow ? createPalette( actions, colors ) : undefined )
      .concat( createCanvas( actions ) )
  )

この例ではパレットの表示,非表示の切り替えを行うときに要素の再生成を行なってしまう.canvas 要素も削除され新しい canvas 要素に置き換えられてしまう.それに伴い描写した内容も画面から削除される.

これは patch 関数で要素を生成する際に,以前のノードと現在のノードのタグ名を見て同じでなければ再生成するといった処理を行なっているために起こっている.これは前回の要素を使い回すために要素の順番を保つか,canvas のノードに key を指定することで解決できる.

const view = ( state, actions ) =>
  h( 'div'
   , {}
   , [ createCanvas( actions )
     , h( 'br' )
     , createPaletteToggle( state, actions )
     , state.isShow ? createPalette( actions, colors ) : '' ]
  )
const createCanvas = actions =>
  h( 'canvas', {
    width: 700,
    height: 400,
    key: 'canvas',
    oncreate: actions.setContext,
    onremove: ( element, done ) => {
      actions.unsetContext()
      done()
    },
  } )

const view = ( state, actions ) =>
  h( 'div'
   , {}
   , [ createPaletteToggle( state, actions )
     , h( 'br' )
     ].concat( state.isShow ? createPalette( actions, colors ) : undefined )
      .concat( createCanvas( actions ) )
  )

要素の入れ替え

リストの要素を key があるときとないときで比べて,要素の入れ替わりを確認してみる.

const view = ( state, actions ) =>
    h( 'ul', {}, state.players.map( player => h( 'li', {}, [player.name] ) ) )

リストの要素に対して player.name という key をつける.

const view = ( state, actions ) =>
    h( 'ul', {}, state.players.map( player => h( 'li', { key: player.name }, [player.name] ) ) )

key がない場合,同じタグ名であるので要素はそのままに,値のみが入れ替わっている.
スクリーンショット 2017-12-30 22.37.02.png
だが,key があるとその要素自体が入れ替わっていることがわかる.
スクリーンショット 2017-12-30 22.35.10.png

感想

1,2 時間じゃ読めませんでした.

27
22
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
27
22