【Vue.js】defineProps はなぜ import 文を書かずに使えるのか【マクロ】
〜某日〜
<script setup>
import { ref, computed } from "vue";
const props = defineProps({ count: Number });
const double = computed(() => props.count * 2);
const message = ref("Hello, Vue.js");
</script>
<template>
<div>
<p>{{ message }}</p>
<p>{{ props.count }}</p>
<p>{{ double }}</p>
</div>
</template>
・
・
・
・
・
・
・
なんで defineProps は import しなくても使えるの?????
そんなことを思った方はいませんか?今日はその疑問を解決して差し上げます。
🚩 初めに
前回は Vue のコンパイラについて 解説してみました。
今回は Vue.js の script setup について学び始めて方が、よく疑問に思うであろう「defineXXXX はなんで import 書かないんだ!!」と言う疑問について解説していきます。
📚 公式ドキュメントを読んでみる
まずは公式ドキュメントを読んでみましょう。
どうのような説明がされているでしょうか?
これらは
<script setup>
の中で自動的に利用できるようになっています:
defineProps と defineEmits は、
<script setup>
内でのみ使用可能なコンパイラーマクロです。インポートする必要はなく、<script setup>
が処理されるときにコンパイルされます。
この説明を見てみるに、「<script setup>
内でのみ使用可能」で「インポートする必要はなく」 と言うことがわかります。
しかし、少し気になる単語があります。
・
・
・
「コンパイラーマクロ」
🤔
コンパイラーマクロとはなんでしょうか?
🤔 コンパイラーマクロとは
defineProps
や defineEmits
などは 「コンパイラーマクロ」 というものの代表例です。
このコンパイラーマクロが何なのかというと、一言で簡潔に言うなら「コンパイル時に JavaScript に展開される擬似的な関数」です。
「コンパイル時」 の意味
コンパイル時に何かが行われているというのはわかりますが、それはなんのコンパイルで、いつ行われるものでしょうか?
「コンパイラ」が何なのか、何のためにあるのかというのは前回解説しました。
今回も例に漏れずこれのコンパイラが関与してきます。
そうです Single File Component (.vue ファイル) のコンパイラです。
この .vue ファイルのコンパイルがいつ行われるかというと、「.vue ファイルが読まれた時」です。
具体的には、 import 文に記述されその import 文が読み込まれた時です。
import MyComp from "./MyComp.vue"; // この行が読み込まれた時に MyComp.vue は JavaScript にコンパイルされる
// .
// .
// .
この読み込みをフックして、Vue.js が実装したコンパイラが動きます。
「擬似的な関数」の意味
「擬似的な」というのがどういう意味かというと、これらのマクロはコンパイル時に展開されるため、実際に存在する関数ではないということです。
defineProps<{ count: number }>();
見た目は完全に JavaScript の関数ですが、コンパイル後、この関数はコンパイラによって処理されてしまうので関数としては存在しません。
では、コンパイル後はどのような形をしているのでしょうか?
🏃♂️ SFC Playground で確認してみる
Single File Component がどのような JavaScript にコンパイルされるのかは、SFC Playground で確認することができます。
以下のような簡単なコンポーネントを見てみましょう。
<script setup lang="ts">
defineProps<{ count: number }>();
const emit = defineEmits<{
(event: "click"): void;
}>();
</script>
<template>
<div>
<p>{{ count }}</p>
<button type="button" @click="emit('click')">Click</button>
</div>
</template>
コンパイルされたコードの注目するべきところは、props オプションと emit オプションです。
const __sfc__ = _defineComponent({
__name: "App",
props: {
count: { type: Number, required: true }, // props オプション
},
emits: ["click"], // emit オプション
setup(__props, { expose: __expose, emit: __emit }) {
__expose();
const emit = __emit;
const __returned__ = { emit };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
});
defineProps
や defineEmits
は綺麗さっぱりいなくなっているのがわかります。
代わりに、props
オプションと emits
オプションが追加されています。
つまり、Vue のコンパイラは、
defineProps<{ count: number }>();
を
props: {
count: { type: Number, required: true },
},
に、
const emit = defineEmits<{
(event: "click"): void;
}>();
を
emits: ["click"],
に変換していることがわかります。
これで、「コンパイル時に JavaScript に展開される擬似的な関数」 という意味がわかったでしょうか?
Vue のコンパイラが解析し、関数ではなく Vue の JavaScript コードに展開してしまうのです。
実際に defineProps や defineEmits という関数は存在しないのです。
これで、「なんで import しなくても使えるの?」という疑問が解決したのではないでしょうか?
コンパイラが勝手に名前を探してやってることなので、import する必要がないのです。
ここまでで、今日説明したいメインの部分はおしまいです。 ここから下はコラムです。
多少難しいかもしれないですが、完全に理解しきらなくても OK です。
💡 コラム1. ところで、なんで 「マクロ」 なの?
コンパイラーマクロがどういう機能なのかがなんとなくわかったところで、次はその名前についてです。
「コンパイラー」なのはわかるとして、「マクロ」とはなんなのでしょうか?
答えから言ってしまうと、これはプログラミング界隈の慣習です。
プログラミング界隈ではしばしば、コンパイル時に展開する(される)ものを「マクロ」と呼びます。
その代表例は「C 言語」です。
C 言語はコンパイル型の言語ですが、「コンパイル」の前に「プリプロセス」という工程があります。
マクロはこのプリプロセスで展開されます。
C 言語では #define
でマクロを定義することができます。
#define PI 3.14
#define AREA(r) PI * r * r
int main() {
double r0 = 10;
double area = AREA(r0);
return 0;
}
そして、実行ファイルを作成するときはまずプリプロセスが走り、PI
は 3.14
に、AREA(r0)
は 3.14 * r0 * r0
に展開されます。
int main() {
double r0 = 10;
double area = 3.14 * r0 * r0;
return 0;
}
これでやっと C 言語のソースコードが完成したので、これを C コンパイラにかけて実行ファイルを生成します。(実際にはもっと複雑な工程があります)
これを見ると、「え、ただの定数や関数じゃないの?」と思うかもしれません。
しかしマクロの役割は、「コードの展開を記述すること」です。ただの関数ではありません。
次の例を見てみましょう。
#include <stdio.h>
#define DEFINE_X int x = 10;
int main() {
DEFINE_X
printf("%d\n", x);
return 0;
}
このコードをプリプロセスで展開すると、
以下のようになります。
int main() {
int x = 10;
printf("%d\n", x);
return 0;
}
このように、マクロではコードの展開を記述することができます。
通常の関数では変数を定義したりすることができません。
#include <stdio.h>
void defineX() {
int x = 10;
}
int main() {
defineX();
printf("%d\n", x); // x は defineX のローカル変数なのでここでは使えない
return 0;
}
マクロは本当にソースコードを展開してしまいます。そんなイメージです。
Vue のコンパイラも考え方は似ていて、コンパイル(ビルド)時にコードを展開することで便利さを提供しています。
よく考えてみて欲しいのですが、
defineProps<{ count: number }>();
というコードが、
props: {
count: { type: Number, required: true },
},
に変換されることはかなり奇妙なことです。
というのも、通常ビルドステップ行われることはせいぜい TypeScript の型情報を落としたり、複数のファイルをまとめたりする程度です。
この考えに則ると、
defineProps<{ count: number }>();
は
defineProps();
に変換されるのが自然です。({ count: number }
はただの型情報ですし、defineProps という関数の呼び出しは普通コンパイル後も残っているはずです)
しかし、Vue のコンパイラは defineProps<{ count: number }>();
というコードの型情報や呼び出しを解析し、それを props: { count: { type: Number, required: true } },
というコードに展開してしまうのです。
これが、「コンパイラーマクロ」 と呼ばれる所以です。
💡 コラム 2. 歴史的背景
最近 Vue を始めた方はもしかすると script setup から入門したかもしれません。
そんな方は props: { count: { type: Number, required: true } }
のような記述を見てもあまりピン来ないかもしれません。
script setup が出たのは v3.2 からとそこそこ最近で、それまでは実は props や emit とというものはオプションとして定義指定した。
👇 script setup が登場する前の Vue のコード
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
count: { type: Number, required: true },
},
emits: ["click"],
setup(props) {
// ...
// ここにメインの処理を書く
},
});
</script>
なんなら、このオプションは Composition API が登場する前から存在していました。
いわゆる Options API と呼ばれるものです。
👇 Composition API が登場する前の Vue のコード
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
count: { type: Number, required: true },
},
emits: ["click"],
data() {
return {
// データ定義
};
},
methods: {
// メソッド定義
handleClick() {
console.log(this.count);
this.$emit("click");
},
},
});
</script>
このコードをもっとユーザーが書きやすくするために、コンパイラに機能を増やして DX
を向上させたのが script setup だったのです。 つまり、defineXXX
は大体、最終的にはこの script setup
以前で使われていたオプションに変換されるためのショートハンドなのです。
※ API 選択を Options
に変更してご覧ください。
💡 コラム 3. 他のマクロの結果も見てみよう
defineProps, defineEmits 以外にも、script setup で使えるマクロはいくつかあります。
一応、この辺りに実装されているのでこの辺から探すことができます。
3.4 で追加された defineModel ももちろんその一つです。
SFC Playground でいろんなマクロの結果を見てみましょう!
const { count = 0 } = defineProps<{ count?: number }>()
console.log(count)
// compiled 👇
props: {
count: { type: Number, required: false, default: 0 }
},
setup(__props) {
console.log(__props.count)
}
defineExpose({ a: 'hello' })
// compiled 👇
setup(__props, { expose: __expose }) {
__expose({ a: 'hello' }) // ここ
}
defineOptions({
name: 'MyComponent',
data() {
return {
a: 'hello'
}
}
})
// compiled 👇
...{
name: 'MyComponent',
data() {
return {
a: 'hello'
}
}
},
const slots = defineSlots<{
default(props: { msg: string }): any;
}>();
// compiled 👇
import { useSlots as _useSlots } from "vue";
const slots = _useSlots();
const model = defineModel()
// compiled 👇
props: {
"modelValue": {},
"modelModifiers": {},
},
emits: ["update:modelValue"],
const model = defineModel('myModel')
// compiled 👇
props: {
"myModel": {},
"myModelModifiers": {},
},
emits: ["update:myModel"],
これで、コンパイラマクロの理解はバッチリだと思います 😉
★ 番外編
実は Vue Macros と言うライブラリがあります。
こちらは Vue.js Core Team Member の Kevin さんが作っているもので、公式には取り込まれていないいろんなマクロが実装されています。
探してみると面白いマクロが見つかるかもしれません!ぜひ!
Discussion