Hyperappを読む

読んだのでまとめる．
# Hyperapp とは
[2018 年は Hyperapp の年だ](https://qiita.com/JorgeBucaran/items/c48446babe0627e25ee6) を読むとわかる．
# 読んでみる
読みやすさのために少し書き換えている．実際のコードは [GitHub - hyperapp/hyperapp](https://github.com/hyperapp/hyperapp/tree/c906e06fde5867f725341742156a77c164a3a156) にある．

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

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

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

```javascript
// { 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](https://preactjs.com) の `render` 関数の引数である `replaceNode` と同じ役割を担っている（と教えてもらいました）．

この `replaceNode` は preact の [API Reference](https://preactjs.com/guide/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. 

```javascript
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` として使用される．

```javascript
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` 関数を実行しないようになっている．

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

function render(){
  patchLock = !patchLock
  ...
}
```
`lifecycle` はコールバック関数を配列で持っている．ノードの `oncreate` や `onremove` などのプロパティが関数のとき，それらを後に出てくる `render` 関数内で取り出して実行する．
### `init` 関数
`init` 関数は `actions` の関数をラップする．また，もし `actions` が入れ子になっていて `state` は入れ子になっていないときは `state` を `actions` の形に合うように拡張する．

```javascript
// 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` も同じように入れ子になる．

```javascript
// Action

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

```javascript
// State

{
  count: {}
}
```
このとき，例えば `actions.count.up` を呼び出すと `state.count` が `actions.count.up` の引数に渡される．`actions.count.up` の返り値も `state.count` で保持される．
### `render` 関数
`render` 関数は `app` 関数に引数として渡された `view` 関数を使い `state` と `actions` からノードを生成する．その後 `patch` 関数に生成したノードを渡す．最後に `lifecycle` の中に関数があれば実行する．

```javascript
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` などの関数を使用して要素の生成，更新を行なっている．これは最後の方にまとめる．

```javascript
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](https://github.com/hyperapp/hyperapp/blob/c906e06fde5867f725341742156a77c164a3a156/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.

```javascript
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` のプロパティを，それ以外は以前のノードのプロパティを参照する．

```javascript
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](https://gist.github.com/calmery/17343206c69a7e5a78a0ac4f19fbe67b) で見ることができる．

```javascript
//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` を指定することで解決できる．

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

```javascript
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` があるときとないときで比べて，要素の入れ替わりを確認してみる．

```javascript
const view = ( state, actions ) =>
    h( 'ul', {}, state.players.map( player => h( 'li', {}, [player.name] ) ) )
```
リストの要素に対して `player.name` という `key` をつける．

```javascript
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](https://qiita-image-store.s3.amazonaws.com/0/211005/b2e08de0-3b5e-9222-bb36-3f9bfc0419d5.png)
だが，`key` があるとその要素自体が入れ替わっていることがわかる．
![スクリーンショット 2017-12-30 22.35.10.png](https://qiita-image-store.s3.amazonaws.com/0/211005/1e0179a8-ddb1-58b3-0643-a4053f7a7644.png)
# 感想
1，2 時間じゃ読めませんでした．