読んだのでまとめる.
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
関数は state
,actions
,view
,container
を引数として受け取る.container
は document.body
などの要素のこと.
この関数内にある変数の root
は preact の render
関数の引数である 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
はコールバック関数を配列で持っている.ノードの oncreate
や onremove
などのプロパティが関数のとき,それらを後に出てくる render
関数内で取り出して実行する.
init
関数
init
関数は actions
の関数をラップする.また,もし actions
が入れ子になっていて state
は入れ子になっていないときは state
を actions
の形に合うように拡張する.
// 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.count
が actions.count.up
の引数に渡される.actions.count.up
の返り値も state.count
で保持される.
render
関数
render
関数は app
関数に引数として渡された view
関数を使い state
と actions
からノードを生成する.その後 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
関数では createElement
,setElementProp
,updateElement
,removeElement
などの関数を使用して要素の生成,更新を行なっている.これは最後の方にまとめる.
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
関数
引数として element
,name
,value
,oldValue
を受け取って element
の name
プロパティに value
を設定する.name
が style
だったときは oldValue
と value
を合わせてから設定する.
updateElement
関数
引数として element
,oldProps
,props
を受け取る.oldProps
と props
でプロパティの値が異なっていれば setElementProp
で更新する.プロパティの名前が value
または checked
の場合は,element
のプロパティを,それ以外は以前のノードのプロパティを参照する.
if( props[name] !== ( 'value' === name || 'checked' === name ? element[name] : oldProps[name] ) ){
setElementProp(element, name, props[name], oldProps[name])
}
removeElement
関数
要素の削除を行う.ノードのプロパティに onremove
があるときには,削除する element
と done
を引数として渡す.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
がない場合,同じタグ名であるので要素はそのままに,値のみが入れ替わっている.
だが,key
があるとその要素自体が入れ替わっていることがわかる.
感想
1,2 時間じゃ読めませんでした.