Riot.js のソースコードをダラダラ読んでいく
この規模のソースコードの全量を読んだことないので、ちゃんと理解進むかわからないけど、空いた時間を見つけてだらだらと読んで行きます。
昔のバージョンでは Riot って仮想 DOM だと説明されているようだったけど、
最新バージョンの公式サイトのトップページでは
Fast expressions bindings instead of virtual DOM memory performance issues and drawbacks.
と説明されてて、そのあたりの仕組みが実際どうなってるのか知りたい。
この @riotjs/dom-bindings を追っていけばわかってくるかな。
ちなみに蛇足かもしれないけど、 Riot のコンポーネントの中で this.$()
で要素を取得する機能は、 このライブラリに依存してるみたい。
久々にここを自分で読み直していて、「この依存関係」どこでみつけたんだっけ?ってなったので実際のコードで使われているところの例を貼っておく
@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
でコンパイルしたコードにも似たものが見えてて、なにしているのかは何となく理解できる。
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
}
]
}]
template
メソッドの定義を見たところ,第一引数は文字列とプレーンな HTML であれば問題なさそうですね.
* @param {string|HTMLElement} html - template string
簡単なデモ用意してみました(=゚ω゚)ノ
コード例まで!ありがとうございます!
頂いたコード例が、公式ドキュメントのその後を読むための助けになってすごい助かりました!!ありがとうございます!!
ちなみに、いただいたコードサンプル微妙に scope.greeting
で渡された文字が表示されてないっぽくて、
どうやら、p
の中に作っておいた <span></span>
が なぜかchildNodeIndex: 0
としてヒットしてないみたいでした。
そこで、ちゃんと scope.greeting
で渡したテキストが表示されるように、いただいたサンプルを修正してみました。
el.appendChild(document.createElement("span"));
を
el.appendChild(document.createTextNode("<!---->"));
に変えただけなんですけど、それでこういう挙動になるということは、
p
要素(selector: "p"
)の中の 0 番目のノード(childNodeIndex: 0
)がテキストノード(type: expressionTypes.TEXT
) じゃないと正しく機能しない、ってことなんですかね…🤔
コメントありがとうございます!自分の方でも確認しましたが,仰っていただいたように 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
とあるように,input
要素などじゃないとだめなようで,実際にやってみたら動きました↓↓↓
確認ありがとうございます〜!そしてまたコード検証まで!助かります〜! 🙌
よく template()
のサンプルコードに <!---->
という空のコメントがあるのが不思議だったんですが、これは書き換える対象部分が TextNode じゃないとならないからだったんですね〜!
腑に落ちましたっ!!😄
Bindings
についての説明があった。
A binding is simply an object that will be used internally to map the data structure provided to a DOM tree.
Bindings
は 内部で DOM ツリーにデータ構造をマップするために使われるシンプルなオブジェクト とのこと。
ほほう。
そのオブジェクトのプロパティについてさらに説明がある。
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
}
]
これなわけだ。
この調子で読んでいって、全量読み切って自分が理解するのどれくらいかかるんだろうか、とちょっと思ったけど、気長にやればいいか…別にだれのためでもないし☺️
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
については
Object containing all the type of bindings supported
「バインディングに対応する全タイプを含むオブジェクト」とのこと。
このタイプについては、後の方に説明があるので一旦詳細は割愛。
なんかちょっと混乱してきた。
なんでこの type
が Number 型なのかよくわからずコードを見に行ってみたら @riotjs/util
の中の 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
}
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()
の引数に指定するような文字列ってことかな。
実際にバインディングの例を見ていく
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
っていうプロパティを、実際に差し替える値として評価する。
…みたいなことだと理解した。
ふむふむ。なるほどね。
Simple Binding の次の例、 Simple Binding Expressions を見てみる。
なんかソースを読むというより、内部のモジュールの README 読んでるだけ、みたいな気もしないでもないが気にしないことにする。
この Simple Binding Expressions というのが、上の Simple Binding とどう違うのか、ちゃんと理解できてないけど、とりあえず読み進めてみる。
Simple Binding の一部(Expression)について説明している、と言う感じかな?