vuejs/core のソースコードを探検する楽しさ
はじめに
フロントエンドエンジニアのhiroです。
エンジニアになって半年が過ぎました。
表題にあるとおり、この記事では vuejs/core のソースコードを探検してみたいと思います。
ソースコードの探検が目的のため、実装の詳しい解説自体は行いません。あらかじめご了承ください。
目的
本記事はubugeeeiさんの素晴らしい本であるchibivueに出会い、ubugeeeiさんご本人にVueについて直接教えて頂き、Vueのソースコードに興味を持ったことがきっかけで執筆しています。chibivueはVueの基本的な機能を自分で実装することで、Vueについての理解を深め、vuejs/coreのソースコードが読めるようになることを目的とされている本です。
エンジニア経験の浅い私にとってVueのソースコードを理解することは難しいですが、全部が理解できなくてもなんとなく 「こういう実装になっているんだ!」 とか 「こういう設計になっているんだ!」 などと少しでも理解に近づけた時はとても楽しいものです。また、普段業務では見かけない書き方を知れたりとコーディングの引き出しを増やす楽しさもあります。
このような楽しさを多くの人に知って頂きたく、本記記事では vuejs/core のソースコードを一緒に探検して頂くことを目的としています。くどいですが実装の詳細を解説する内容ではないため、実装の詳細について学習したい方は是非chibivueに挑戦してみてください!
対象読者
- Vueのソースコードを見てみたい方
- Vueが好きな方(嬉しい💚)
- TypeScriptがなんとなくわかる方
まずはドキュメントを読んでみる
例としてcreateApp()から探検していきましょう!
まずはソースコードではなく使用方法を見ていきます。
import { createApp } from 'vue'
// ルートコンポーネントを単一ファイルコンポーネントからインポートする
import App from './App.vue'
const app = createApp(App)
こちらは公式ドキュメントのVue アプリケーションの作成-> ルートコンポーネントに記載されているコードです。文字どおりcreateAppはVueアプリケーションの作成に使用されています。
createAppの説明をドキュメントで確認してみると
アプリケーションインスタンスを作成します。
とあります。なんとなくわかるという方もいると思います。でもまだ少し抽象的ですよね。もう少しドキュメントを読んでみましょう。
- 型
function createApp(rootComponent: Component, rootProps?: object): App
- 詳細
第1 引数はルートコンポーネントです。第 2 引数(省略可能)は、ルートコンポーネントに渡される props です。
今回は型に焦点を置いてcreateApp()の実態を追っていきたいと思います。
まず型について、createApp()自体の戻り値はApp型となっています。ここではアプリケーションを作成するために必要な様々な機能を持ったオブジェクト型と理解してください。詳しい説明は後述するので、大体のイメージが掴めればOKです。
次に引数です。第 1 引数はルートコンポーネントとなっていますが、要するにアプリケーション全体のエントリーとなるコンポーネントのことです。Vueプロジェクトを立ち上げるとルートコンポーネントはApp.vueになっていると思います。省略可能な第 2 引数は、このルートコンポーネントが props を持っている場合はここで渡してあげましょうということですね。
まだ曖昧な理解かもしれませんが、なんとなく戻り値と引数が分かったので、先程よりはイメージできてきたのではないでしょうか。
vuejs/core のソースコードを探検する
ここではApp型とComponent型について、vuejs/core のソースコードを見ながらもう少し解像度を上げていきたいと思います。
App型
まずはApp型を見ていきましょう。
コードはこちらになります。
version・config・useなど様々なプロパティやメソッドが定義されていることがわかります。
これが
アプリケーションを作成するために必要な様々な機能を持ったインスタンス
ということです。
文字通り様々なプロパティやメソッドが定義されていますが、今回はmount()について確認してみます。
createApp()しただけではブラウザには何も表示されません。ドキュメントのコードにもあるとおり
import { createApp } from 'vue'
const app = createApp(/* ... */)
app.mount('#app')
app.mount('#app')と書くことでブラウザに表示されます。
まとめると
-
App型とはmount()やprovide()などVueの様々な機能の型を定義するオブジェクト型
となります。
少し解像度が上がってきましたね!この調子でComponent型も確認していきましょう!
Component型
Component型は実際に手を動かしながら確認していきます。
今回はVue3のこちらのcodesandboxを利用していきます。
まずmain.jsを以下のように書きます。
import { createApp } from "vue";
import App from "./App.vue";
// createApp()の引数であるAppコンポーネントを見てみる
console.log(App);
const app = createApp(App);
app.mount("#app");
開発者ツールのコンソールを見て見ると
{name: 'App', components: Object, __hmrId: '7ba5bd90', __file: 'src/App.vue', render: ƒ}
が出力されていると思います。
ここでは、Appコンポーネントがname・components・__hmrId・__file・renderを持っていることがわかりました。また、.vue ファイルがただのオブジェクトに解決されていることがわかりますね。
さっそくソースコードを見てみましょう。
Component型はConcreteComponent | ComponentPublicInstanceConstructorとなっていますね。
ConcreteComponent型は
ComponentOptions | FunctionalComponent型となっています。このように追っていくと...
ComponentOptions型の中に先程のname・components・renderを見つけることができました。よく見るとsetup・inheritAttrsなどもありますね👀
FunctionalComponent型を追っていくと...
上記のComponentInternalOptions型を継承していることがわかり、ここで__hmrId・__hmrIdを確認することができました。
まとめると
-
Component型はConcreteComponent型が適用されており、名前のとおりコンポーネントそのもののオブジェクト型
となります。
動作確認をして小さな発見をしてみる
Appコンポーネントにsetup()を追加してみると
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
export default {
+ setup() {},
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
};
</script>
{name: 'App', components: Object, __hmrId: '7ba5bd90', __file: 'src/App.vue', setup: ƒ, render: ƒ}
オブジェクトにsetupが追加されていることが確認できますね!
次にAppコンポーネントのtemplate部分を削除してみましょう。
- <template>
- <img alt="Vue logo" src="./assets/logo.png" />
- <HelloWorld msg="Hello Vue 3 in CodeSandbox!" />
- </template>
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
export default {
setup() {},
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
};
</script>
{name: 'App', components: Object, __hmrId: '7ba5bd90', __file: 'src/App.vue', setup: ƒ}
そうすると今度はrenderがなくなりました。
つまりtemplate構文はコンパイルされてrender()になっていたということがわかります。
ドキュメントの説明を見てみると
単一ファイルコンポーネントのような、あらかじめコンパイルされたテンプレートは、ビルド時に render オプションにコンパイルされます。
となっていますね。
試しにtemplate構文で書いていたものをrender()に書き換えてみます。
<script>
import HelloWorldVue from "./components/HelloWorld.vue";
+ import { h } from "vue";
+ import logo from "./assets/logo.png";
export default {
setup() {},
name: "App",
components: {
HelloWorld: HelloWorldVue,
},
+ render() {
+ return h("div", [
+ h("img", {
+ alt: "Vue logo",
+ src: logo,
+ }),
+ h(HelloWorldVue, {
+ msg: "Hello Vue 3 in CodeSandbox!",
+ }),
+ ]);
+ },
+ };
</script>
{name: 'App', components: Object, __hmrId: '7ba5bd90', __file: "src/App.vue", setup: ƒ, render: ƒ}
renderが戻りましたね!
createAppオブジェクトのmount()を呼び出すことでVueがブラウザに表示されていた
と説明しましたが、render()がVueコンポーネントの表示を行っていたということになります。
長くなりましたがcreateApp()は以下のようなことをしていることがわかりました!
-
createApp()はVueの様々な機能を持ったインスタンスを生成するAPIである - 生成されるインスタンスは引数のコンポーネントに依存する
まとめ
vuejs/core のソースコードを確認するひとつの流れとして
- ドキュメントの解説を読んで
- コードを書き換えて
- コンソールを見て
- ソースコードと見比べて
- ドキュメントの解説を改めて読んでみる
と色々な発見があったり、理解が深まったりして楽しめると思います!
おわりに
ここまで読んで頂きありがとうございました。
誤りや質問があればコメントやTwitter(@hiro)にご連絡頂ければと思います。
まだまだ未熟者ですが、少しずつVueコミュニティに貢献できるようになれたらと思っています!
Discussion
hiroさん
vuejs/core のソースコード読まれてるなんてすごい、、、😇
勝手にすごく勇気をもらったというか勉強のモチベーション上がりました。
読んでいて気になったことがあったので念のためコメントいたします🙇♂️
Component型にある「今回はVue3のこちらのcodesandboxを利用していきます。」に埋めてあるリンク先に遷移すると、「Sandbox not found」と表示されてしまいます。
ぼくの勘違いだったら大変申し訳ないんですが、確認いただけると嬉しいです!
読ませてもらってぼくもVue勉強します!
よろしくお願いします。
ナイトウさん
コメントありがとうございます!
まだまだ理解できないコードの方が多くいですよ〜涙
モチベーションの向上に貢献できたのなら大変嬉しく思います!
こちら大変失礼いたしました🙇♂️
リンクに誤りがあったので修正いたしました。
お手数ですがご確認頂ければと思います。
お互いVueの勉強楽しんでいきましょう〜!
確認いただきありがとうございます!