🐿️

chibivue航海日誌

2024/12/24に公開

Vue Advent Calendar 2024 24日目の記事です。
https://qiita.com/advent-calendar/2024/vue

はじめに

皆さん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

URL

ここは何も難しいことをしていません。
document.querySelector で対象をとって innerHTML をしているだけですね。

パッケージの設計

URL

ここが最初のつまづきポイントかなと思います。

パッケージの設計の図

ここでやったことは↑の図のように 初めてのレンダリングと createApp API でやった内容を runtime-coreruntime-dom に分割しています。

この変更だけの差分

こうすることにより、runtime-core に DOMが依存しないようになります。一方で、前回までは main.ts から一足飛びで createApp の定義元に飛べましたが、隠蔽されるようになりました。

「この render はどの render だ??」といった形で今後複雑さを増していきますが、miroを頼りに進められたらと思います。

HTML要素をレンダリングできるようにしよう

URL

HTML要素をレンダリングできるようにしようの図

現状では単純にtextをrederingしているだけなので、HTML tagを表示できるようにします。

  • textからobjectを扱うようになる(h関数による変換)
  • nodeOpsにsetTextしかないので、createElementを追加
  • ↑に対応するrenderVNodeを追加した

なお、h関数だけ直接mainから呼んでいますが、お察しの通りこれは後述のタイミングで切り替わります(DOMに関する処理なので...?)。

イベントハンドラや属性に対応してみる

URL

イベントハンドラや属性に対応してみるの図

現状では単にHTMLが表示されるだけで、styleやadd eventに対応できていないので対応します。

  • patchPropsとmodulesが登場します
    • modulesが実際の処理
    • patchPropsはそれらをまとめる処理
      • createRendererの引数として渡して、renderVNodeの中で実行する

リアクティビティシステムの前程知識

URL

リアクティビティシステムの前程知識の図

この章ではどのようにしてリアクティブな動きを再現しているのかという説明がメインです。あとは次章のための実践が少し。

  • 実践に関しては↑の図の通り。setupを追加しています。これは次に使うstateの層を設けいています
  • あとはProxyがメインです。これは公式docs以上に分解することが難しい。単純にobjectに対しての処理をtrapできるので、そこにupdateの処理を入れるぐらいの認識でいいと思います。

小さいリアクティビティシステムを実装してみる

URL

小さいリアクティビティシステムを実装してみる

この章を理解する為にひたすら print debugしてcodeの細かい処理をreturnを追いかけました。

自分の中で理解できた流れがこちら↓
リアクティブシステムの進行図

地道に追いかけることで、targetMapにどういう値が実際に格納されているのか分かります。
例えば↓のように仕込むと

export function trigger(target: object, key?: unknown) {
  console.log('trigger', targetMap, target, key)
  const depsMap = targetMap.get(target)

consoleに表示されたtargetMap

とincrementをclickするたびにkeyが更新されていくのが分かりますね!

小さい仮想 DOM

URL

小さい仮想 DOMの図

もはや1つの図にすることが難しくなってきたので変更箇所だけ拡大した図↓
小さい仮想 DOMの変更点を拡大した図

前回までだとDOMを全て書き直しているので、VirtualDOMを使って実装する

  • createVNodeとnormalizeVNodeを追加する
    • normalizeVNodeはtextをVNodeにする処理
    • createVNodeは何をするの?
      • 端的にいうとVNodeに置き換える処理
        • textにsymbolが使えるようになる
        • propsにnullを入れれる
        • hostを持てる(自分自身のDOMへの参照)
        • それぞれで扱うinterfaceを統一するような動きもしていそう
  • patch関数
    • mount(初回rendering) / patch(2回目以降) / これらの処理をまとめて precess
    • 全体の更新処理もpatchという
    • renderVNodeの代わりにpatchが入る
      • renderVNodeは HTML要素をレンダリングできるようにしよう で対応したもの
      • HTML tagを受け取って parseしていた
    • なぜhostを持っておく必要があるのか?
      • hostCreateElementで挿入している
        • el = vnode.el = hostCreateElement(type as string)
      • patchPropが出てくるけど、これも HTML要素をレンダリングできるようにしよう で追加したものなので混乱しないように
  • createAppApiに書いていたupdateをrendererに移した

コンポーネント指向で開発したい

URL

Props の実装

URL

Emit の実装

URL

(コンポーネント ~ Props ~ Emit は元々1つのpageだったため便宜的に1箇所にimageとmemoをまとめています。ご了承ください)

コンポーネント指向で開発したいの図

結構衝撃を受けた章でした。てっきりroot的なところで持っていると思っていたのですが、それぞれのコンポーネントで管理しているんですね!
どこに何を追加した??が分かりずらかったですが、↑のように1つずつ追いかけていって理解しました。

  • ざっくりコンポーネント単位で実装できるようになった(コンポーネントを受け取れるようになった)
    • rootComponent = createAppがあるcomponent
    • container = div#app のこと
  • なぜparentNodeが必要なのか?
    • component単位でrenderするようになったので、それぞれの親の取得が必要

テンプレートコンパイラを理解する

URL

(説明のみなので省略)

テンプレートコンパイラを実装する

URL

テンプレートコンパイラを実装するの図

変更箇所の拡大図↓
テンプレートコンパイラを実装するの拡大図

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 を書きたい

URL

もっと複雑な 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の方が理解できていればすっとできました!!!

parseでやったことと同じことをgenerate側でもやるの詳細図

データバインディング

URL

データバインディングの図

コンポーネント指向で開発できるようになったものの、以前作ったリアクティブや仮想DOMの恩恵が受けられません。なので、setup内に書いたコードをtemplateから参照できるようにします。

  • setupStateを追加する
    • 今までは引数を持てないし、関数を返すだけだった
    • 引数を持てるようにして、objectを返せるようにした
  • with(説明のみ)
    • ようはscopeを貫通するという話です
  • マスタッシュ構文( {{ ←こういうやつ)に対応する
    • ASTに追加する。するとparseやcodegenにも対応が必要
  • genNodeにwith文を追加する
    • internalRender funcが引数を受け取れるようにする
    • renderingまで完成
  • directiveにも対応する
    • マスタッシュ構文同様にAST/parse/codegenそれぞれに追加する

Single File Component で開発したい (周辺知識編)

URL

Minimum Exampleとして大きいsectionはこれで最後になります。
今は文字列として書いているHTMLを普段僕らが見慣れているSFCに変換していくsectionです。

コンパイルの処理をいつどこで噛ませるのか?という説明がviteのhello world的なコードと共に紹介されています。
理解できるとかなり感動するのでぜひ進めてみてください。

SFC パーサ ~ template, script, style

※こちらのsectionに関してはこれまで進めてきたのであれば、document通りに進めるだけで理解できると思いますので省略します。

さいごに

chibivue URL

本projectを作成された ubugeeei さん。
その他、Vueに関するコミュニティ活動を行っている皆様に感謝を申し上げます。

年末年始暇だ!という方は是非chibivueにチャレンジしてみてください。オススメです!!!

告知!!!

chibivue勉強会を開催しています

毎週水曜 12:00 ~ 12:55
お昼ご飯のラジオ感覚にでもぜひ!!!

https://stmk-study.connpass.com/event/333968/

乗り遅れた方向けに振り返り会も開催しています

2024/12/27(金) 20:00 ~ 22:00

https://connpass.com/event/340070/

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion