Hyperapp のソースコードを読む

# Hyperappとは

Hyperapp GitHub ページ
[GitHub - hyperapp/hyperapp: 1 KB JavaScript library for building frontend applications.](https://github.com/hyperapp/hyperapp)

> Web アプリのフロントエンド用 JavaScript ライブラリ。React, Preact, Vue といった代表的なものよりもずっと小さく、1 KB という超軽量サイズ。他のライブラリに依存することなく使えて、さらにスピードもある :fire:
> 
> Elmアーキテクチャーに基づいてて、アプリケーション設計はElmやReact、Reduxと似てるけど、ボイラープレートは少ないし、TypeScriptにも対応して、とにかくシンプル。
> [2018 年は Hyperapp の年だ - Qiita](https://qiita.com/JorgeBucaran/items/c48446babe0627e25ee6)

## 実装サンプル

[hyperapp/hello-world.md at master · hyperapp/hyperapp · GitHub](https://github.com/hyperapp/hyperapp/blob/master/docs/introduction/hello-world.md)

```js
const state = {
  count: 0
}

const actions = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
}

const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, state.count),
    h("button", { onclick: () => actions.down(1) }, "–"),
    h("button", { onclick: () => actions.up(1) }, "+")
  ])

window.main = app(state, actions, view, document.body)
```

実行すると、下記のようなHTMLが表示されて、ボタンを押すと数値を増減する。

```html
<div>
  <h1>0</h1>
  <button>-</button>
  <button>+</button>
</div>
```

どことなく `react` などで見たような形だが、より少ないコードで書かれている。


## Hyperapp のコンセプト

[hyperapp/README.md at master · hyperapp/hyperapp · GitHub](https://github.com/hyperapp/hyperapp/blob/master/docs/concepts/README.md)

| コンセプト | メモ |
|---|---|
| Virtual Nodes | 仮想Nodeは `h` 関数もしくはJSXなどを使う。DOMイベント、ライフサイクルイベント、キーを含める事が出来る。 |
| Keys | Nodeに一意のキーを付けて管理できる。 |
| Components | 仮想Nodeのコンポーネント化が可能。 |
| Lifecycle Events | 仮想Nodeの作成・更新・削除時のライフサイクルイベントを設定できる。 |
| Sanitation | 仮想Node使わない時はXSSに気を付けて。 |
| Hydration | ハイドレーション。描画を全てJSで行うとSEO的に不利なので、初期表示のHTMLがソースコードに書いてあってもOK。 |

これらの事を最小のコードで実現しているのが Hyperapp と考えて良い。

# Hyperappのソースコードを読む

ソースコードはこちら。
[hyperapp/index.js at master · hyperapp/hyperapp · GitHub](https://github.com/hyperapp/hyperapp/blob/master/src/index.js)

`export` されている関数は`h()`と`app()`の2つだけ。

## `h()` 関数

[仮想Node](https://github.com/hyperapp/hyperapp/blob/master/docs/concepts/vnodes.md)を作る。
↓こう書くと

```js
h("div", {id: 'app'}, [
  h("h1", {class: 'title'}, 'Title'),
  h("button", { onclick: () => {hoge: 'hoge'} }, "–"),
  h("button", { onclick: () => {hoge: 'fuga'} }, "+"),
  'some text'
]);
```

↓こうしてくれる。

```js
{
  name: "div",
  props: {
    id: "app"
  },
  children: [
    {
      name: "h1",
      props: { class : "title" },
      children: ["Title"]
    },
    {
      name: "button",
      props: { onclick: () => {hoge: 'hoge'} },
      children: ["-"]
    },
    {
      name: "button",
      props: { onclick: () => {hoge: 'fuga'} },
      children: ["+"]
    },
    "some text"
  ]
}
```

あんまり解説するポイントがない。

`h()` 関数をネストさせた場合、まずは内側（第三引数の配列内）の `h()` 関数が順番に実行され、一番外側の `h()` 関数は一番最後に実行される。

尚、JSXの場合はそのままではJavaScriptとして実行できないのでトランスパイルが必要だが、`h()` 関数はそのまま動く。


## `app()` 関数

### `app()` 関数の冒頭

```js
export function app(state, actions, view, container) {
  var patchLock
  var lifecycle = []
  var root = container && container.children[0]
  var node = vnode(root, [].map)
  
  repaint(init([], (state = copy(state)), (actions = copy(actions))))
  
  return actions

  // 内部で利用する関数群
}
```

幾つかの変数を宣言して、`init()` しつつ `repaint()` を呼び出して終了。

この時、`var node = vnode(root, [].map)` でDOMを仮想Node化して[ハイドレーション](https://github.com/hyperapp/hyperapp/blob/master/docs/concepts/hydration.md)を実現している。

`app()` の引数及び、冒頭で宣言された変数が、内部で利用する関数群におけるクロージャの変数として利用されている。

### `init()` 関数

`actions` 関数の実行時に `state` が返却されたら `state` を更新して `repaint()` するように登録している。

```js
 function init(path, slice, actions) {
    for (var key in actions) {
      typeof actions[key] === "function"
        ? (function(key, action) {
            actions[key] = function(data) {
              slice = get(path, state)

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

              if (data && data !== slice && !data.then) {
                repaint((state = set(path, copy(slice, data), state, {})))
              }

              return data
            }
          })(key, actions[key])
        : init(
            path.concat(key),
            (slice[key] = slice[key] || {}),
            (actions[key] = copy(actions[key]))
          )
    }
  }
```

まず、最後の方のコードについて解説する。

```js
      typeof actions[key] === "function"
        ? // 省略
        : init(
            path.concat(key),
            (slice[key] = slice[key] || {}),
            (actions[key] = copy(actions[key]))
          )
```

`actions[key]` が `function` じゃない場合は `path` にキーを追加して、再度 `init()` を呼び出している。これは、`actions` と `state` で同じキーを使ってグループ化できる様にしている模様。（今のところ公式ドキュメントのサンプルには載ってない）
このため、`init()` 内での`state` や `actions` は全体的に、その時のキーを処理するような形になっている。例えば `slice` 変数は、`state` からその時のキーに対応する連想配列を取り出したものだ。

次に下記のコードについて。

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

直訳だと「アクションの実行で関数が戻ってきたら、`slice`, `actions`を引数にしてその関数を実行する」。
これはサンプルの所々に出てきた、アロー関数が2つ繋がっている書き方（関数を返す関数）に対応している。外側の関数は呼び出した時の引数、内側の関数は `slice`, `actions` を引数として実行される。これにより、ビュー側では `state` や `actions` を引数に渡さなくても良くなっている。

例えばサンプルに出てくる、この関数を返す関数が書かれているアクション。

```js
const actions = {
  down: value => state => ({ count: state.count - value }),
}
```

このアクションに対応するビュー側は `onclick: () => actions.down(1)` という呼び出しで、引数は1つだけだ。しかし、Hyperappが自動的に `state` と `actions` を引数にして二回目の関数を実行するので、アクション内ではこれらの変数を利用できる。

> 補足 ： アロー関数に慣れてない方の為に、上記のアクションを今までの書き方にするとこうなる。
> 
> ```js
> const actions = {
>   down: function(value) {
>     return function(state) {
>       return { count: state.count - value };
>     }
>   }
> }
> ```


最後にこのコード。

```js
              if (data && data !== slice && !data.then) {
                repaint((state = set(path, copy(slice, data), state, {})))
              }

              return data
```

関数の返却値が `slice` では無かったら、`state` を更新、そして `repaint()` している。また、`!data.then` という条件により、プロミスや `async` の時は `repaint()` は実行されず、`return data`してコールバックを繋げている。


### `repaint()` 関数と `render()` 関数

まず、`patchLock` 周りについて。

```js
  function render(next) {
    patchLock = !patchLock
    // 処理
  }

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

この様に2つの関数に分けられているのは、無駄な処理実行を減らす為だ。`repaint()` 内で `setTimeout` して `render()` を別スレッドで実行する事で、`repaint()` を呼び出した関数の一連の処理を先に終わらせている。また、`patchLock` を使うことで、一連の処理内で `repaint()` が何度も呼び出されても `render()` が呼び出されるのは1回だけとなる。

`render()` の処理を追っていく。

```js
  function render(next) {
    next = view(state, actions)

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

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

`next = view(state, actions)` で、最新の `state` を引数にして、新しい仮想Node `next` を作成している。尚、`view()` は仮想Nodeを作成する関数だ。サンプルでは下記のようになっている。

> ```js
> const view = (state, actions) =>
>   h("div", {}, [
>     h("h1", {}, state.count),
>     h("button", { onclick: () => actions.down(1) }, "–"),
>     h("button", { onclick: () => actions.up(1) }, "+")
>   ])
> ```

次に `root = patch(container, root, node, (node = next))` で、node を最新のものに入れ替えつつ、`patch()` 関数を実行してビューを更新。
最後の `while ((next = lifecycle.pop())) next()` は、`patch()` で追加・更新されたNodeのライフサイクルイベントを実行している。

ライフサイクルイベントの公式ドキュメントはこちら。
[hyperapp/lifecycle-events.md at master · hyperapp/hyperapp · GitHub](https://github.com/hyperapp/hyperapp/blob/master/docs/concepts/lifecycle-events.md)

### `patch()` 関数

新旧の仮想Nodeの差分を見ながら適切に実DOMに反映している。
また、ライフサイクルイベントの実行、Nodeによって一意な Key による処理なども行っている。

1. 新旧Nodeが同じなら何もしない
2. 旧Nodeが `null` なら新Node挿入
3. 新旧NodeのNode名が同じなら Keys を考慮しつつ良きようにアップデート
4. テキストNodeなら `nodeValue` にセット
5. それ以外なら新Nodeを挿入しつつ旧要素を削除

Keys の公式ドキュメントはこちら。
[hyperapp/keys.md at master · hyperapp/hyperapp · GitHub](https://github.com/hyperapp/hyperapp/blob/master/docs/concepts/keys.md)

### その他関数

上記のメイン関数から呼ばれる補助的な関数。

- `copy()`
- `set()`
- `get()`
- `getKey()`
- `setElementProp()`
- `createElement()`
- `updateElement()`
- `removeChildren()`
- `removeElement()`

以上。


----


# 感想など

作者が意図した通り、学習コストの低さはとても良いと思った。

> オープンソースの世界ではソースはパブリックだからもちろん誰でも読めるけど、読めるのと「全部把握している」のはまた別だ。使いながらも、いつもブラックボックスの部分がある。でも 300行なら、バックヤードの全てを理解した上で使うことができる。それを実現したかった。
> [2018 年は Hyperapp の年だ - Qiita](https://qiita.com/JorgeBucaran/items/c48446babe0627e25ee6)

何が起きるか分かった上で、自分のコードを実装できる。また、シンプルで最小限であるが故に、Hyperapp で実装したアプリを他のもっと手厚いフレームワークへ移行するのは難易度が低いと思う。（逆の場合は、削ぎ落とされる部分をどう補うか考えねばならず難しいかもしれない。）
モックアップなどを手軽に試したい時は、先ずは Hyperapp で作ってみても良いのではないだろうか。

僅か1kbのコードでコアコンセプトを実現するのは、素直に凄い。細かなテクニックも勉強になった。