Open19

Riot.js のソースコードをダラダラ読んでいく

GENKIGENKI

https://github.com/riot

この規模のソースコードの全量を読んだことないので、ちゃんと理解進むかわからないけど、空いた時間を見つけてだらだらと読んで行きます。

GENKIGENKI

https://github.com/riot/riot/blob/ca53665dc97ef645174abf4fe4a4442c77403040/src/riot.js#L1

昔のバージョンでは Riot って仮想 DOM だと説明されているようだったけど、
最新バージョンの公式サイトのトップページでは

https://riot.js.org/#performant-and-predictable

Fast expressions bindings instead of virtual DOM memory performance issues and drawbacks.

と説明されてて、そのあたりの仕組みが実際どうなってるのか知りたい。

GENKIGENKI

ちなみに蛇足かもしれないけど、 Riot のコンポーネントの中で this.$() で要素を取得する機能は、 このライブラリに依存してるみたい。

https://github.com/biancojs/query#readme

GENKIGENKI

https://github.com/riot/dom-bindings#readme

@riotjs/dom-bindings の README を読んでみる。
なんだかコードをよむというより単純に README の翻訳みたいになっちゃうけども。

import { template, expressionTypes } from '@riotjs/dom-bindings'

// テンプレートを作成
const tmpl = template('<p><!----></p>', [{
  selector: 'p',
  expressions: [
    {
      type: expressionTypes.TEXT,
      childNodeIndex: 0,
      evaluate: scope => scope.greeting,
    },
  ],
}])

// #app という要素をマウント先のターゲットとして変数化
const target = document.getElementById('app')

// `template` という API の `mount()` を使ってマウント
const app = tmpl.mount(target, {
  greeting: 'Hello World'
})

このあたりは、 @riotjs/cli でコンパイルしたコードにも似たものが見えてて、なにしているのかは何となく理解できる。

GENKIGENKI

https://github.com/riot/dom-bindings#templatestring-array

template(String, Array) についての説明。

The template method is the most important of this package. It will create a TemplateChunk that could be mounted, updated and unmounted to any DOM node.

いわく、 template() メソッドは最も重要とのこと。
任意の DOM 要素にマウント・更新・アンマウント可能な TemplateChunk というデータのまとまり?を作成する役割らしい。

ふむふむ。

A template will always need a string as first argument and a list of Bindings to work properly. Consider the following example:

const tmpl = template('<p><!----></p>', [{
  selector: 'p',
  expressions: [
    {
      type: expressionTypes.TEXT,
      childNodeIndex: 0,
      evaluate: scope => scope.greeting
    }
  ],
}])

最初の引数に文字列、その後にちゃんと動く Bindings のリストが必要とのこと。
Bindings のリストというのはこれ(↓)のことかな?

[{
  selector: 'p',
  expressions: [
    {
      type: expressionTypes.TEXT,
      childNodeIndex: 0,
      evaluate: scope => scope.greeting
    }
  ]
}]
GENKIGENKI

コード例まで!ありがとうございます!

頂いたコード例が、公式ドキュメントのその後を読むための助けになってすごい助かりました!!ありがとうございます!!

ちなみに、いただいたコードサンプル微妙に scope.greeting で渡された文字が表示されてないっぽくて、
どうやら、p の中に作っておいた <span></span> が なぜかchildNodeIndex: 0 としてヒットしてないみたいでした。

そこで、ちゃんと scope.greeting で渡したテキストが表示されるように、いただいたサンプルを修正してみました。

https://codesandbox.io/s/es6-playground-forked-9rpo0c?file=/src/index.js

el.appendChild(document.createElement("span"));

el.appendChild(document.createTextNode("<!---->"));

に変えただけなんですけど、それでこういう挙動になるということは、
p 要素(selector: "p")の中の 0 番目のノード(childNodeIndex: 0)がテキストノード(type: expressionTypes.TEXT) じゃないと正しく機能しない、ってことなんですかね…🤔

Kiyohito Keeth KuwaharaKiyohito Keeth Kuwahara

コメントありがとうございます!自分の方でも確認しましたが,仰っていただいたように type: expressionTypes.TEXT を指定した場合は HTML 要素では動作しないようです.それが証拠に上記の自分のデモを以下のように変更してみたのですが

const el = document.createElement("p");
  const content = document.createTextNode("Hi there and greetings!");
+ el.appendChild(content);
  el.appendChild(document.createElement("span"));
- el.appendChild(content);
+ el.appendChild(document.createTextNode("<!---->"));

// Create the app template
const tmpl = template(el, [
  {
    selector: "p",
    expressions: [
      {
        type: expressionTypes.TEXT,
-       childNodeIndex: 0,
+       childNodeIndex: 2,
        evaluate: (scope) => scope.greeting
      }
    ]
  }
]);

これでも動作しましたので,やはり expressionTypes.TEXT を指定したなら TextNode じゃないと動作しないですね(=゚ω゚)ノ

ちなみに type: expressionTypes.VALUE を指定して HTML 要素ならどうかと思いましたが,こちらも動作しませんでした.

It should be used only for form elements

README

とあるように,input 要素などじゃないとだめなようで,実際にやってみたら動きました↓↓↓

https://codesandbox.io/embed/es6-playground-3cz1vc?fontsize=14&hidenavigation=1&theme=dark

GENKIGENKI

確認ありがとうございます〜!そしてまたコード検証まで!助かります〜! 🙌

よく template() のサンプルコードに <!----> という空のコメントがあるのが不思議だったんですが、これは書き換える対象部分が TextNode じゃないとならないからだったんですね〜!
腑に落ちましたっ!!😄

GENKIGENKI

https://github.com/riot/dom-bindings#bindings

Bindings についての説明があった。

A binding is simply an object that will be used internally to map the data structure provided to a DOM tree.

Bindings内部で DOM ツリーにデータ構造をマップするために使われるシンプルなオブジェクト とのこと。
ほほう。

そのオブジェクトのプロパティについてさらに説明がある。

GENKIGENKI

Bindings.expressions

  • type: Array<Expression>
  • required: true
  • description: array containing instructions to execute DOM manipulation on the node queried

データ型は ExpressionType の配列
ExpressionType の説明はあとにでてくるっぽいので一旦、「ほうほう」とだけ思っておく。
省略不可で、クエリされたノードでDOM操作を実行するための命令を含む配列とのこと。
うん。
それが前述された例にでてきたコードの中でいう

[
  {
    type: expressionTypes.TEXT,
    childNodeIndex: 0,
    evaluate: scope => scope.greeting
  }
]

これなわけだ。

GENKIGENKI

この調子で読んでいって、全量読み切って自分が理解するのどれくらいかかるんだろうか、とちょっと思ったけど、気長にやればいいか…別にだれのためでもないし☺️

GENKIGENKI

Bindings.type

上記の expressions 以外は、Bindings のプロパティとしては必須ではない模様。
ということで、必須ではないプロパティの一つ目、 type。

  • type: Number
  • default:bindingTypes.SIMPLE
  • optional: true
  • description: id of the binding to use on the node queried. This id must be one of the keys available in the > - bindingTypes object

まず、Number型で、デフォルトが bindingTypes.SIMPLEで、
クエリーされたノードで使用されるバインディングのIDとのこと。

ちなみに bindingTypes については

https://github.com/riot/dom-bindings#bindingtypes

Object containing all the type of bindings supported

「バインディングに対応する全タイプを含むオブジェクト」とのこと。
このタイプについては、後の方に説明があるので一旦詳細は割愛。

なんかちょっと混乱してきた。

GENKIGENKI

なんでこの type が Number 型なのかよくわからずコードを見に行ってみたら @riotjs/util の中の expression-types.js がこういう内容だったからだった。

https://github.com/riot/util/blob/main/expression-types.js

export const ATTRIBUTE = 0
export const EVENT = 1
export const TEXT = 2
export const VALUE = 3

export default {
  ATTRIBUTE,
  EVENT,
  TEXT,
  VALUE
}
GENKIGENKI

Bindings.selector

  • type: String
  • default: binding root HTMLElement
  • optional: true
  • description: property to query the node element that needs to updated

つづいては selecter プロパティ。

String 型。
デフォルトはルートの HTMLElement。
更新する必要があるノード要素のクエリー文字列とのこと。 querySelector() の引数に指定するような文字列ってことかな。

GENKIGENKI

実際にバインディングの例を見ていく

Simple Binding

https://github.com/riot/dom-bindings#simple-binding

Simple Binding は DOM 構造を変更せず、単一の DOM 要素(ノード)を対象としたバインディング、とのこと。
コード例は以下の様な感じ

const pGreetingBinding = {
  selector: 'p',
  expressions: [{
    type: expressionTypes.TEXT,
    childNodeIndex: 0,
    evaluate: scope => scope.greeting,
  }]
}

template('<article><p><!----></p></article>', [pGreetingBinding])
/**
 * ↑
 * 公式ドキュメントのサンプルコードがが間違ってるようなので、自分が正しいと思う記述に修正。
 * (公式にはドキュメントの修正PR出したので多分そのうちマージされるはず…)
 * https://github.com/riot/dom-bindings/pull/24/files
**/

Simple Binding は以下の式(expression)を含む必要があるとのこと

  • attribute 更新するノードの属性値
  • event 設定するイベントハンドラー
  • text 更新するノードの中身
  • value 更新するノードの値

ここはつまり、 type: expressionTypes.TEXT のところが 、

  • type: expressionTypes.ATTRIBUTE
  • type: expressionTypes.EVENT
  • type: expressionTypes.TEXT
  • type: expressionTypes.VALUE

だったりするという事だと思う。

ドキュメント曰く、上述のコード例は「pタグのコンテンツのみを更新するためのバインディングを作成した例」で、「マウントするたびにpタグの中のコメント部分が、バインディングを実行した値に置き換わるとのこと。

つまり、<article><p><!----></p></article> という要素に対して、
selector: 'p'p タグの中の…
childNodeIndex: 0 ← 0番目の子要素(コメント部分)に
type: expressionTypes.TEXT ← 渡された値をテキストノードとして反映するモードで(?)
evaluate: scope => scope.greeting ← 渡される値(オブジェクト)の greeting っていうプロパティを、実際に差し替える値として評価する。

…みたいなことだと理解した。

ふむふむ。なるほどね。

GENKIGENKI

Simple Binding の次の例、 Simple Binding Expressions を見てみる。

なんかソースを読むというより、内部のモジュールの README 読んでるだけ、みたいな気もしないでもないが気にしないことにする。

https://github.com/riot/dom-bindings#simple-binding-expressions

この Simple Binding Expressions というのが、上の Simple Binding とどう違うのか、ちゃんと理解できてないけど、とりあえず読み進めてみる。
Simple Binding の一部(Expression)について説明している、と言う感じかな?