chibivue航海日誌
Vue Advent Calendar 2024 24日目の記事です。
はじめに
皆さんchibivueというprojectをご存知ですか??
chibivueはVueを最小限で作る(車輪の再発明する)ことで
- Vue.js についての理解を深める
- Vue.js の基本的な機能を実装できるようになる
- vuejs/core のソースコードを読めるようになる
↑といった項目の達成を目的に作成されたprojectです(参照)。
僕自身はVueを2024年から触り始めたのですがその際にこの「chibivue」の存在を知りました。
動機
僕はVueの理解を深める為にchibivueにとりかかってみたのですが、まぁ難しい。1人で進めていたら数章進んだところで止まってしまいました。
結局は会社の人を巻き込んでなんとかゴールできたという感じです。
やはり車輪の再発明は難しいのですが、だからこそ効果が高いとも言えます。
しかし、人によっては他の人を巻き込みずらい人もいるかと思います。
そんな時に、この記事を使うことでchibivueを1人でも進められるようになればいいなと思って書きました。
2周目を回った際のmemo書き(≒航海日誌)だと思って読み進めていただけますと。
難しいを分解する
僕自身難しいと感じることが多かったのですが、何が難しいのでしょうか。
会社の人たちと1周目を終えた後その答えを探すべくソロで2周目に突入しました。
そこで1つ見つけたのが「今どこで何をしているか?」ということがわかりにくいなと感じました。この説明はどこのfileのことをいっているんだと迷子になります。
全体像がわからないということではないです。それはむしろ丁寧に説明されています。
全体像から詳細に落とすときの段階にもう一つクッションがあると分かりやすいと思うのです。
また、chibivue本体では「vuejs/core のソースコードを読めるようになる」が目的となっています。そのため、DIされたコードを追いかけていきます。
これは普段製品開発をしている「職業エンジニア」の人たちがあまり出会わないコードというのもそれに拍車をかけているのかなと思います。
(n=1ですが、特にフロントエンドの領域にでプロダクト開発しているとDIされているコードを触る機会が殆どない気がします。あったら珍しいと思う。imo)
ですので、2周目ではmiroとnotionを片手に自分なりにchibivueの地図を作りながら進めていきました。それをざっと書いています。
想定読者
- chibivue前から気になっていたけど、躊躇していた人
- chibivue前に一度やったけど、挫折した人 / いまいち理解できなかった人
本章スタート!!!
それでは、前置きが長くなりましたが、本章を一緒に読み進めていきましょう!!!
道中に出てくるmiroの矢印の実線と破線の違いは心の眼で読み取ってください。僕のメンタルモデルみたいなものです。
(付箋の黄色はblock, 青→赤→橙の順でそのsectionで追加された内容)
※範囲は Minimum Exampleのみ。ちょっと一息はまとめなので省略しています
初めてのレンダリングと createApp API
ここは何も難しいことをしていません。
document.querySelector
で対象をとって innerHTML
をしているだけですね。
パッケージの設計
ここが最初のつまづきポイントかなと思います。
ここでやったことは↑の図のように 初めてのレンダリングと createApp API
でやった内容を runtime-core
と runtime-dom
に分割しています。
こうすることにより、runtime-core
に DOMが依存しないようになります。一方で、前回までは main.ts から一足飛びで createApp の定義元に飛べましたが、隠蔽されるようになりました。
「この render はどの render だ??」といった形で今後複雑さを増していきますが、miroを頼りに進められたらと思います。
HTML要素をレンダリングできるようにしよう
現状では単純にtextをrederingしているだけなので、HTML tagを表示できるようにします。
- textからobjectを扱うようになる(h関数による変換)
- nodeOpsにsetTextしかないので、createElementを追加
- ↑に対応するrenderVNodeを追加した
なお、h関数だけ直接mainから呼んでいますが、お察しの通りこれは後述のタイミングで切り替わります(DOMに関する処理なので...?)。
イベントハンドラや属性に対応してみる
現状では単にHTMLが表示されるだけで、styleやadd eventに対応できていないので対応します。
- patchPropsとmodulesが登場します
- modulesが実際の処理
- patchPropsはそれらをまとめる処理
- createRendererの引数として渡して、renderVNodeの中で実行する
リアクティビティシステムの前程知識
この章ではどのようにしてリアクティブな動きを再現しているのかという説明がメインです。あとは次章のための実践が少し。
- 実践に関しては↑の図の通り。setupを追加しています。これは次に使うstateの層を設けいています
- あとはProxyがメインです。これは公式docs以上に分解することが難しい。単純にobjectに対しての処理をtrapできるので、そこにupdateの処理を入れるぐらいの認識でいいと思います。
小さいリアクティビティシステムを実装してみる
この章を理解する為にひたすら print debugしてcodeの細かい処理をreturnを追いかけました。
自分の中で理解できた流れがこちら↓
地道に追いかけることで、targetMapにどういう値が実際に格納されているのか分かります。
例えば↓のように仕込むと
export function trigger(target: object, key?: unknown) {
console.log('trigger', targetMap, target, key)
const depsMap = targetMap.get(target)
とincrementをclickするたびにkeyが更新されていくのが分かりますね!
小さい仮想 DOM
もはや1つの図にすることが難しくなってきたので変更箇所だけ拡大した図↓
前回までだとDOMを全て書き直しているので、VirtualDOMを使って実装する
- createVNodeとnormalizeVNodeを追加する
- normalizeVNodeはtextをVNodeにする処理
- createVNodeは何をするの?
- 端的にいうとVNodeに置き換える処理
- textにsymbolが使えるようになる
- propsにnullを入れれる
- hostを持てる(自分自身のDOMへの参照)
- それぞれで扱うinterfaceを統一するような動きもしていそう
- 端的にいうとVNodeに置き換える処理
- patch関数
- mount(初回rendering) / patch(2回目以降) / これらの処理をまとめて precess
- 全体の更新処理もpatchという
- renderVNodeの代わりにpatchが入る
- renderVNodeは
HTML要素をレンダリングできるようにしよう
で対応したもの - HTML tagを受け取って parseしていた
- renderVNodeは
- なぜhostを持っておく必要があるのか?
- hostCreateElementで挿入している
el = vnode.el = hostCreateElement(type as string)
- patchPropが出てくるけど、これも
HTML要素をレンダリングできるようにしよう
で追加したものなので混乱しないように
- hostCreateElementで挿入している
- createAppApiに書いていたupdateをrendererに移した
コンポーネント指向で開発したい
Props の実装
Emit の実装
(コンポーネント ~ Props ~ Emit は元々1つのpageだったため便宜的に1箇所にimageとmemoをまとめています。ご了承ください)
結構衝撃を受けた章でした。てっきりroot的なところで持っていると思っていたのですが、それぞれのコンポーネントで管理しているんですね!
どこに何を追加した??が分かりずらかったですが、↑のように1つずつ追いかけていって理解しました。
- ざっくりコンポーネント単位で実装できるようになった(コンポーネントを受け取れるようになった)
- rootComponent = createAppがあるcomponent
- container = div#app のこと
- なぜparentNodeが必要なのか?
- component単位でrenderするようになったので、それぞれの親の取得が必要
テンプレートコンパイラを理解する
(説明のみなので省略)
テンプレートコンパイラを実装する
変更箇所の拡大図↓
h関数からtemplate構文に置き換えた。
- parseとcodegenの実装。codegenからは文字列が来るのでそれはFunctionに喰わせることで動的に関数を生成する
- parse: HTML文字列にh関数を加える
- codegen: ↑これを文字列にする
- コンパイラは2種類存在する
- buildプロセスと、runtimeプロセス。今作ったのはruntime
- これらの共通する処理はcoreに書かれる
- runtimeにはcompile-sfc/compile-dom が存在する
- h関数がmainから直接呼ばれなくなった
- h関数はこのタイミングコード上で実行されなくなった。runtime上で実行されるようになった
もっと複雑な HTML を書きたい
複数階層あるHTML文字列やstyle tagなども描画できるようにする
- ASTの導入(文字列を解析する)
- ASTと聞くとギョッとしますが、そんなに身構えなくても大丈夫
<div>
<p id="p1">
<span id="s1">...</span>
<span id="s2">...</span>
</p>
<p id="p2">
<span id="s3">...</span>
<span id="s4">...</span>
</p>
</div>
みたいなHTMLがあったときに、深さ優先探索で以下の処理が行われる(という雑な理解)
1. div ancestors: []
2. #p1 ancestors: [div]
3. #s1 ancestors: [div, #p1]
4. #s2 ancestors: [div, #p1]
5. #p1の閉じタグ発見 ancestors [div]
6. #p2 ancestors [div]
7. #s3 ancestors [div, #p2]
8. #s4 ancestors [div, #p2]
9. #p2の閉じタグ発見 ancestors []
10. divの閉じタグ発見 → おわり
- ASTを元にrender関数を生成する
- parseでやったことと同じことをgenerate側でもやる
- ↓のようにparseとcodegenは全く同じ形なので、parseの方が理解できていればすっとできました!!!
データバインディング
コンポーネント指向で開発できるようになったものの、以前作ったリアクティブや仮想DOMの恩恵が受けられません。なので、setup内に書いたコードをtemplateから参照できるようにします。
- setupStateを追加する
- 今までは引数を持てないし、関数を返すだけだった
- 引数を持てるようにして、objectを返せるようにした
- with(説明のみ)
- ようはscopeを貫通するという話です
- マスタッシュ構文(
{{
←こういうやつ)に対応する- ASTに追加する。するとparseやcodegenにも対応が必要
- genNodeにwith文を追加する
- internalRender funcが引数を受け取れるようにする
- renderingまで完成
- directiveにも対応する
- マスタッシュ構文同様にAST/parse/codegenそれぞれに追加する
Single File Component で開発したい (周辺知識編)
Minimum Exampleとして大きいsectionはこれで最後になります。
今は文字列として書いているHTMLを普段僕らが見慣れているSFCに変換していくsectionです。
コンパイルの処理をいつどこで噛ませるのか?という説明がviteのhello world的なコードと共に紹介されています。
理解できるとかなり感動するのでぜひ進めてみてください。
SFC パーサ ~ template, script, style
※こちらのsectionに関してはこれまで進めてきたのであれば、document通りに進めるだけで理解できると思いますので省略します。
さいごに
本projectを作成された ubugeeei さん。
その他、Vueに関するコミュニティ活動を行っている皆様に感謝を申し上げます。
年末年始暇だ!という方は是非chibivueにチャレンジしてみてください。オススメです!!!
告知!!!
chibivue勉強会を開催しています
毎週水曜 12:00 ~ 12:55
お昼ご飯のラジオ感覚にでもぜひ!!!
乗り遅れた方向けに振り返り会も開催しています
2024/12/27(金) 20:00 ~ 22:00
Discussion