Riot.js ソースコード完全解説 v3対応版


昨年「[Riot.js ソースコード完全解説](http://qiita.com/cognitom/items/eb6d56485fe517ccd9ee)」を書いたのですが、この1年でいろいろ状況が変わってしまいました。v2の間に相当コードが書き換わり、Riotも若干ぽっちゃり系に...(他の同種のライブラリに比べれば可愛いものですが)。そんな中、朗報です。近日v3がリリースされ、コードが整理されます。

- 参考: [v3のロードマップ](https://github.com/riot/riot/issues/1694)

本稿では、改めて「ソースコード解説」を試みたいと思います。 **2016年7月現在** で`next`ブランチにあるコードをもとに説明するので、日々変わってしまうけど、ご参考まで。オススメのコースは、次の通り。

- この記事に目を通して全体像をつかむ
- 自力でソースコードを斜め読みし、もうちょっと深く理解する
- プルリクエストしたい部分について、深読みする
- がしがし[プルリクエスト](https://github.com/riot/riot/pulls)

![riot-map.png](https://qiita-image-store.s3.amazonaws.com/0/1858/a31d523d-dc50-73af-5c7c-191e67413b23.png "riot-map.png")

スクリプトの`import`/`export`関係をざっくり図示してみました。以下にも主要ファイルを挙げておきます。

- 公開API: `lib/riot.js`
- Riotのコア部分: `lib/browser/tag/core.js`
- タグ/DOM操作の多くはユーティリティ関数で: `lib/common/util.js`
  - タグに関するもの: `tag-helper.js`
  - DOMに関するもの: `dom-helper.js`
  - その他: `misc.js`
- タグそのもの: `lib/browser/tag/tag.js`
  - DOMのパース (というよりはtraverse): `parse.js`
  - 表現(expression): `if.js`, `each.js`, `named.js`など
  - 更新: `update.js`
- 定数: `lib/browser/common/global-variables.js`

## 基本方針

### コーディングスタイル

1. 行末のセミコロンなし
2. 短いのが正義
3. `==`や`!=`は挙動を理解して使う
4. `if`文や`for`文で、省略可能であれば`{}`を使わない
5. 演算子の評価順を正しく考慮。冗長な`()`を使わない
6. 独自の関数を優先的に使う(場面がある)
  - `Object.assign()`ではなく、`extend()`
  - `[].forEach()`ではなく、`each()`
7. ES6で書くが、変換時に長くなるClassなどは使わない (現在のところ)

### バンドル方法

- ビルドなどのタスク実行には`make`を使う。(詳細は[Makefile](https://github.com/riot/riot/blob/master/Makefile)参照)
- バンドラは[rollup](http://rollupjs.org/)
- ES6変換は[Babel](https://babeljs.io/)
- `riot-observable`, `riot-tmpl`, `riot-compiler`は外部リポジトリに (本バージョンから、`riot-route`は含まれない)
- 用途別に複数のバージョンを用意
  - `riot.js`: 通常版
  - `riot.min.js`: 上記の圧縮版
  - `riot+compiler.js`: コンパイラ付属版
  - `riot+compiler.min.js`: 上記の圧縮版
  - `riot.csp.js`: CSP(Content Security Policy)対応版
  - `riot.csp.min.js`: 上記の圧縮版

## 用例

解説中のコードについて、次のようにコメント、省略をするケースがあります。

- `// 原文コメント`
- `//// 本稿によるコメント`
- `.... 本稿によるコードの省略`

## tagファイルのコンパイル

コンパイラは当初、Riotのリポジトリに含まれていたのですが、v2.3から別リポジトリになっています。v2.3の時点で全面的に[@aMarCruz](https://github.com/aMarCruz)によって全面的に書き換えられ、ロバストになりました。

- [riot-compiler](https://github.com/riot/compiler)

基本的な実装のアイデアは、2.0当初と同じなので、[昨年のコード解説](http://qiita.com/cognitom/items/eb6d56485fe517ccd9ee#%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9-compilerjs-%E5%85%A8284%E8%A1%8C)を参照してもらえれば、読み解きやすいと思います。新しい`riot-compiler`には「これでもか」というくらいコメントが付いているので、[ソースコード](https://github.com/riot/compiler/blob/master/src/core.js)を直接どうぞ。

- 正規表現だのみの簡易パーサ
- ...だけど、だいぶ揉まれてたので、もうきっと大丈夫 w
- 心配なひとは、[spec](https://github.com/riot/compiler/tree/master/test/specs)を参照してください

### HTML, CSS, JavaScriptの分離

ここではコンパイラのソースには踏み込みませんが、概略だけ説明しておきます。Riot.jsでは、HTMLに若干のテンプレート変数(`{ message }`みたいなやつ)を加えた、tagファイルを使ってカスタムタグを表現します。

```html
<my-tag>
  <p>{ message }</p>
  <script>
    this.message = "Hello world!"
  </script>
  <style>
    p { color: blue }
  </style>
</my-tag>
```

もし上記のtagファイルがあったとすると、次のように内容を分割する必要があります。

- HTMLテンプレート: `<p>{ message }</p>`
- JavaScript: `this.message = "Hello world!"` ※タグの初期化関数
- CSS: `p { color: blue }`

実のところ、Riotのコンパイラが行うのはこの分割と、多少の整形のみです。いわゆる本格的なコンパイラとは異なり、構文解析していないのでシンプルかつ高速です。

コンパイラを通すと、上記のtagファイルは、次のようなJavaScriptのコードに変換されます。(実際には改行なし)

```javascript
riot.tag2(
  'my-tag',
  '<p>{ message }</p>',
  'p { color: blue }',
  '',
  function(opts) { this.message = "Hello world!" }
)
```

プリコンパイルする場合は、各種プリプロセッサに対応します。

- Jade
- TypeScript, CoffeeScript, LiveScript, Babel
- Sass, LESS, Scss, Stylus *※LESSのみブラウザでも可*

## カスタムタグの読み込み

ここからは、コードの内容をしっかり見ていきます。

### riot.tag2()

- カスタムタグを登録する。基本的には手動で使わない。tagファイルを`riot-compiler`でコンパイルすると、`tag2()`を使う形式になる。
- 定義場所: `lib/browser/tag/core.js`

```javascript
function tag2(name, tmpl, css, attrs, fn) {
  if (css) styleManager.add(css, name) //// スタイルマネージャにCSSを登録
  ....
  __TAG_IMPL[name] = { name, tmpl, attrs, fn } //// キャッシュに登録
  ....
  return name
}
```

ここで、`__TAG_IMPL[name]`に入っているのは、こういうデータ:

```javascript
{
  name,  // (1) タグ名
  tmpl,  // (2) HTMLテンプレート
  attrs, // (3) tagファイル内で、タグの属性として定義されているもの
  fn     // (4) 初期化関数
}
```

もし、下記のようなtagファイルだとすると...

```html
<my-tag attr1="abc" attr2="xyz">
  <p>{ message }</p>
  <script>
    this.message = "Hello world!"
  </script>
</my-tag>
```

中身はこう。

```javascript
{
  name:  'my-tag',
  tmpl:  '<p>{ message }</p>',
  attrs: 'attr1="abc" attr2="xyz"', // 配列ではなく文字列な点に注意
  fn:    'this.message = "Hello world!"'
}
```

### riot.tag()

- 手動でtagを組む場合に使う。(あまり推奨されない)
- 基本的な挙動は、`tag2()`と同様だが、後方互換が維持されているため、引数として`css`と`attrs`は省略可能。
- 定義場所: `lib/browser/tag/core.js`

```javascript
function tag(name, tmpl, css, attrs, fn) {
  ....
}
```

## マウントから表示まで

大雑把な流れとしては、次の通り。

1. `riot.tag2()`関数で登録済みの、タグの実装を取得
2. タグをインスタンス化
3. タグの実装(HTMLテンプレート)を`innerHTML`に突っ込む
4. パースする、というよりDOMをトラバースして、解析
5. 与えられたデータで、タグの内容を更新
6. テンプレート変数(expression)を更新
7. マウント完了イベント

以下、処理の中で「幹」になる部分に限定して処理を追っていきます。

*見通しがつきにくくなるので、`each`属性や`if`属性、`<yield />`などの処理については省略しますが、一旦「幹」を理解すれば、枝葉については実際のコードを読むほうが早いでしょう。*

![mounting-tags.png](https://qiita-image-store.s3.amazonaws.com/0/1858/afe2476b-2bfb-a3d5-7add-71b5d16ee108.png "mounting-tags.png")

### riot.mount()

- Riotでタグをマウントするときに使う、公開API。
- `riot.mount('#somewhere', 'my-tag')`のように特定のDOMとタグを指定して使う。
- 内部処理: CSSの挿入後、[mountTo()](#mountto)で実際にタグをマウント。
- 定義場所: `lib/browser/tag/core.js`

```javascript
function mount(selector, tagName, opts) {
  let tags = []
  ....
  styleManager.inject() //// スタイルを`<head>`に挿入
  ....
  //// root(DOM要素)にriotTag(riotタグ)をマウント
  let tag = mountTo(root, riotTag || root.tagName.toLowerCase(), opts)
  ....
  return tags //// マウントしたタグのインスタンスを返す ※複数の場合もある
}
```

### mountTo()

- DOMにRiotタグをマウントし、タグのインスタンスを返す。
- 内部処理: [Tag()](#tag)のインスタンス生成後、[tagMount()](#tagmount)を呼ぶ。
- 定義場所: `lib/browser/util/tag/helper.js`

```javascript
function mountTo(root, tagName, opts) {
  var tag = __TAG_IMPL[tagName] //// タグ実装のキャッシュ
  ....
  
  if (tag && root) tag = new Tag(tag, conf, innerHTML) // インスタンス化

  if (tag && tag.mount) {
    tag.mount(true) //// タグをマウント!
    // add this tag to the virtualDom variable
    if (!contains(__VIRTUAL_DOM, tag)) __VIRTUAL_DOM.push(tag)
  }

  return tag //// インスタンスを返す
}
```

`__TAG_IMPL[tagName]`に入っている内容については、前述の[riot.tag2()](#riottag2)を参照。

### Tag()

- タグを表現する「クラス」 ※クラス的に使われるだけで、実体は関数
- 内部処理: [observable](http://riotjs.com/ja/api/observable/)化、HTMLテンプレートを元に[mkdom()](#mkdom)でDOMを作成
- 定義場所: `lib/browser/tag/tag.js`

```javascript
function Tag(impl, conf, innerHTML) {
  var
    self = observable(this), //// observable化
    opts = inherit(conf.opts) || {}, //// optsそのもの
    parent = conf.parent,
    ....
    expressions = [],
    root = conf.root, //// マウント先のDOM要素
    tagName = conf.tagName || root.tagName.toLowerCase(),
    ....
    dom

  //// すでにマウントされている場合に、マウント解除
  if (impl.name && root._tag) root._tag.unmount(true)
  ....
  defineProperty(this, '_riot_id', ++__uid) //// _riot_idは変更不可
  extend(this, { parent, root, opts }, item)
  defineProperty(this, 'tags', {}) //// tagsプロパティは、子タグのインスタンスへの参照を保持
  //// 指定タグの実装を元にDOMの作成
  //// ※ innerHTMLが、<yield />の内容を置き換えるために渡される点に注意
  dom = mkdom(impl.tmpl, innerHTML)
  ....  
  //// 公開メソッド update, mixin, mount, unmount は上書きできない
  defineProperty(this, 'update', function tagUpdate(data) {....})
  defineProperty(this, 'mixin', function tagMixin() {....})
  defineProperty(this, 'mount', function tagMount(forceUpdate) {...})
  defineProperty(this, 'unmount', function tagUnmount(keepRootTag) {....})
}
```

#### tagMount()

- タグをマウントする
- 内部処理:
  1. tagファイルに書かれた初期化関数(`<script></script>`の中身)を実行
  2. 自分自身(のDOM)をテンプレートとして解析([parseExpressions()](#parseexpressions))
  3. タグの内容を[tagUpdate()](tagUpdate())で更新
  4. 解析済みのDOMを対象に付け替え
  5. マウント完了イベントを発行

```javascript
function tagMount(forceUpdate) {
  ....
  updateOpts(item) //// optsの更新
  if (impl.fn) impl.fn.call(self, opts) //// 初期化関数を実行
  ....
  parseExpressions(dom, self, expressions, false) //// レイアウトの解析
  self.update(....) //// データを元にタグの内容を更新
  ....
  //// domの内部ノードを、rootに付け替え
  while (dom.firstChild) frag.appendChild(dom.firstChild)
  if (root.stub) root = parent.root
  root.appendChild(frag)
  ....
  defineProperty(self, 'root', root)
  self.isMounted = true
  ....
  self.trigger('mount') //// マウント完了イベント
  ....
}
```

#### tagUpdate()

- タグの内容の更新
- 内部処理: 自身をdataの内容で上書きし、[update()](#update)を呼ぶ

```javascript
function tagUpdate(data) {
  ....
  //// ループの中の場合、親要素からデータを継承
  if (isLoop && anonymous) inheritFrom(self.parent)
  ....
  extend(self, data) //// 自身をdataの内容で上書き
  updateOpts()
  if (self.isMounted) self.trigger('update', data) //// 更新前イベント
  update(expressions, self) //// 更新処理の本体
  if (self.isMounted) self.trigger('updated') //// 更新後イベント

  return this
}
```

### mkdom()

- タグの実装をもとに、DOMを作成する
- 内部処理: [mkEl()](#mkel)で要素を生成、`<yield />`の処理をして、[setInnerHTML()](#setinnerhtml)で内容をセット。要素を返す。
- 定義場所: `lib/browser/tag/mkdom.js`

```javascript
function mkdom(templ, html) {
  var el = mkEl(GENERIC, isSVGTag(tagName)) //// 要素の作成
  ....
  //// <yield />がある場合に、htmlで置き換え
  templ = replaceYield(templ, html)

  if (tblTags.test(tagName))
    //// テーブル関係のタグ(theadとか)は特別な処理
    el = specialTags(el, templ, tagName)
  else
    //// それ以外は、innerHTMLに内容をセット
    setInnerHTML(el, templ)

  el.stub = true

  return el
}
```

### parseExpressions()

- [walkNodes()](#walknodes)で、DOMをトラバースしながら、内容に応じて子タグを生成。同時に、テンプレート変数を拾い出しておく。
- 内部処理: 次のように場合分け
  1. テキストノード
  2. each属性あり: [_each()](#_each)に処理をまわす
  3. if属性あり: [IfExpr()](#ifexpr)をインスタンス化
  4. カスタムタグの場合: [getTag()](#gettag)で、カスタムタグであるかの判定をしつつ、タグの実装をを取得。実装をもとに、子タグを生成。
- 定義場所: `lib/browser/tag/parse.js`

```javascript
function parseExpressions(root, tag, expressions, includeRoot) {
  var base = { parent: {children: expressions } } //// 最初のコンテキスト

  walkNodes(root, function(dom, ctx) {
    ....
    //// テキストノードで、テンプレート変数を含む場合
    if (dom.nodeType == 3 .... && tmpl.hasExpr(dom.nodeValue))
      parent.children.push({ dom, expr: dom.nodeValue })
    ....
    //// each属性で繰り返しが設定されている場合
    if (attr = getAttr(dom, 'each')) {
      parent.children.push(_each(dom, tag, attr)); return false
    }
    //// if属性が指定されている場合
    if (attr = getAttr(dom, 'if')) {
      parent.children.push(new IfExpr(dom, tag, attr)); return false
    }
    ....
    //// カスタムタグの場合
    var tagImpl = getTag(dom) //// カスタムタグかどうかを判定
    if (tagImpl && (dom !== root || includeRoot)) {
      var conf = { root: dom, parent: tag, hasImpl: true }
      parent.children.push(initChildTag(tagImpl, conf, dom.innerHTML, tag))
      return false
    }
    //// 通常のHTML要素の場合、属性内のテンプレート変数を処理
    parseAttributes(dom, dom.attributes, tag, (attr, expr) => {
      if (!expr) return
      parent.children.push(expr)
    })

    return { parent }
  }, base)
}
```

### update()

- テンプレート変数(expression)ごとに値を再計算して、タグを更新。
- 内部処理: `tmpl()`で、テンプレート変数を計算。各種場合分けをして、最終的にはdomに内容をセット。
- 定義場所: `lib/browser/tag/update.js`

```javascript
function update(expressions, tag) {
  each(expressions, (expr, i) => {
    var
      dom = expr.dom,
      attrName = expr.attr,
      value = tmpl(expr.expr, tag), //// テンプレートを処理して文字列を得る
      parent = dom && dom.parentNode,
      isValueAttr = attrName == 'value'

    //// 真偽値の場合
    if (expr.bool) value = value ? attrName : false
    //// `null`なら空文字列に
    else if (value == null) value = ''
    //// Riotタグの場合 (_riot_idが付いているかで判断)
    if (expr._riot_id) {
      if (expr.isMounted) expr.update() //// タグの更新
      else { expr.mount(); .... } //// マウントされてなければ、今すぐマウント
      return
    }
    //// Riotタグ以外の場合でも、IfExprや_eachにはupdate()メソッドがあるので、それを実行
    if (expr.update) { expr.update(); return }
    //// 直前の値をoldに保持
    var old = expr.value
    expr.value = value
    ....
    if (isValueAttr && dom.value == value || !isValueAttr && old === value)
      return //// 変更がなければ何もしない
    ....
    //// 元々の属性を削除
    remAttr(dom, attrName)
    //// 関数なら、イベントハンドラとして登録
    if (isFunction(value)) setEventHandler(attrName, value, dom, tag)
    //// show/hide属性の場合
    else if (/^(show|hide)$/.test(attrName)) {
      if (attrName == 'hide') value = !value
      dom.style.display = value ? '' : 'none'
    }
    //// value属性の場合
    else if (attrName == 'value') dom.value = value
    //// 「riot-」で始まる属性名の場合。例: <img riot-src="{ expr }">
    else if (startsWith(attrName, RIOT_PREFIX) && attrName != RIOT_TAG) {
      if (value) setAttr(dom, attrName.slice(RIOT_PREFIX.length), value)
    } else {
      ....
      //// 真偽値属性の場合、それをセット
      if (expr.bool) { dom[attrName] = value; .... }
      //// 属性をセット
      if (value === 0 || value && typeof value !== T_OBJECT)
        setAttr(dom, attrName, value)
    }
  })
}
```

### _each()

Riotのソースリーディングの中で、最大の難関かも...。けど、長すぎるので、略!

```javascript
function _each(dom, parent, expr) {
  ....
}
```


## ユーティリティ関数

### lib/browser/util/misc.js

#### defineProperty()

- `Object.defineProperty()`のショートカット。
- デフォルトは、以下をオプションとし、上書きできないプロパティを付与したい場合に使う。
  - プロパティに列挙せず: `enumerable: false`
  - 変更不可: `writable: false`
  - 設定可能: `configurable: true`

```javascript
function defineProperty(el, key, value, options) {
  Object.defineProperty(el, key, extend({
    value,
    enumerable: false,
    writable: false,
    configurable: true
  }, options))
  return el
}
```

#### each()

- 基本的には、`[].forEach()`的なもの。ただし、`fn`が`false`を返した場合の挙動が異なるので、挙動に注意。

```javascript
function each(list, fn) {
  const len = list ? list.length : 0

  for (let i = 0, el; i < len; ++i) {
    el = list[i]
    if (el != null && fn(el, i) === false)
      i--
  }
  return list
}
```

#### extend()

- ほぼ`Object.assign()`。ただし`writable`かどうかのチェックが入る点が異なる。

```javascript
function extend(src) {
  var obj, args = arguments
  for (var i = 1; i < args.length; ++i) {
    if (obj = args[i]) {
      for (var key in obj) {
        if (isWritable(src, key))
          src[key] = obj[key]
      }
    }
  }
  return src
}
```

### lib/browser/util/tag-helpers.js

[mountTo()](#mountto)については、上述のとおり。

#### getTag()

- `dom`から、カスタムタグの実装を返す。タグ名または、`riot-data-is`あるいは`data-is`属性がキーとなる

```javascript
function getTag(dom) {
  return dom.tagName && __TAG_IMPL[
    getAttr(dom, RIOT_TAG_IS)
    || getAttr(dom, RIOT_TAG)
    || dom.tagName.toLowerCase()
  ]
}
```

### lib/browser/util/dom-helpers.js

#### mkEl()

- `createElement()`のショートカット。SVGを考慮する。

```javascript
function mkEl(name, isSvg) {
  return isSvg ?
    document.createElementNS('http://www.w3.org/2000/svg', 'svg') :
    document.createElement(name)
}
```

#### setInnerHTML()

- `innerHTML`に値をセット。SVGを考慮する。

```javascript
function setInnerHTML(container, html) {
  if (!isUndefined(container.innerHTML))
    container.innerHTML = html
    // some browsers do not support innerHTML on the SVGs tags
  else {
    const doc = new DOMParser().parseFromString(html, 'application/xml')
    const node = container.ownerDocument.importNode(doc.documentElement, true)
    container.appendChild(node)
  }
}
```

#### walkNode()

- 渡されたDOMの内容を順繰りにトラバースして、`fn`の処理をこなす
- 内部処理: まず自身について`fn`を実行し、子要素のすべてについて再帰的に`walkNode()`を呼ぶ
- 定義場所: `lib/browser/util/dom-helpers.js`

```javascript
function walkNodes(dom, fn, context) {
  if (dom) {
    const res = fn(dom, context)
    let next
    // stop the recursion
    if (res === false) return

    dom = dom.firstChild

    while (dom) {
      next = dom.nextSibling
      walkNodes(dom, fn, res)
      dom = next
    }
  }
}
```