core-vapor のディレクトリ構成
リポジトリの用語について
これから行う説明について,vuejs/core と vuejs/core-vapor のどちらもに適応される話は v3 のリポジトリ
として表記します.(例,v3 のリポジトリでは,~~~)
何が core-vapor 固有の話で,何がもと (vuejs/core) からある話なのかを区別することで差分を予想しながら core-vapor の理解に繋げます.
主要なパッケージ
v3 のリポジトリは pnpm workspace によってモノレポで管理されています.
各パッケージは /packages
ディレクトリに配置されています.
packages
そして,それらのパッケージは大きく分けてコンパイラとランタイムの 2 つに分けられます.compiler-
で始まるパッケージはコンパイラに関連するパッケージで,runtime-
で始まるパッケージはランタイムに関連するパッケージです.
- packages/compiler-core
- packages/compiler-dom
- packages/compiler-sfc
- packages/runtime-core
- packages/runtime-dom
core-vapor では新たに compiler-vapor
と runtime-vapor
が追加されています.
また,次に重要なパッケージが reactivity
です.ref
や computed
, watch
などの実装はランタイムパッケージからは独立して @vue/reactivity
として提供されています.
こちらは /packages/reactivity
に配置されています.
そして,Vue.js のエントリとなるパッケージは /packages/vue
に配置されています.core-vapor
においては,これに加え,/packages/vue-vapor
という Vapor Mode のエントリとなるパッケージが追加されています.
全体像:
compiler-core
compiler-core
はその名の通りコンパイラのコア部分を提供します.
コンパイラのパッケージはこれらの他に,compiler-dom
と compiler-sfc
などがありますが,
core は,sfc や dom といった特定の用途や特定の環境に依存しないコアな実装です.
Vue.js にはさまざまなコンパイラが存在しています.
例えば,template
オプションを利用するとランタイム上でテンプレートがコンパイルされます.
createApp({
template: `<div>{{ msg }}</div>`,
setup() {
const msg = ref("Hello, Vue!");
return { msg };
},
}).mount("#app");
しかし,このテンプレートは見てわかる通り,SFC でも同様のテンプレート構文を利用しています.
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("Hello, Vue!");
</script>
<template>
<div>{{ msg }}</div>
</template>
また,これ以外にも HTML の innerHtml として記載したものをコンパイルするケースなど,Vue.js のテンプレートとしてのコンパイルは様々です.
このような様々な用途の共通部分を提供するのが compiler-core
だという理解で概ね問題ありません.
具体的には,template
を render
関数にコンパイルするコアな実装が含まれます.
compiler-dom
Vue.js では,DOM に関する操作やコード生成を行うものは 環境依存である という考えのもと,これらはコアから分離されています.
これは後ほど runtime の方でも登場します.
コンパイラに関して言えば,DOM イベントに関するコードを生成したり,特定の DOM 要素に関するコードを生成したりする実装が含まれます.
Vue.js のイベント修飾子あたりを想像してもらうとわかりやすいかもしれません.
例えば,@submit.prevent
といった修飾子は,
(e: Event) => e.preventDefault()
のようなコードが必要となり,これは DOM API に依存するコード生成です. このようなものを提供するのが compiler-dom です.
例:
29registerRuntimeHelpers({
30 [V_MODEL_RADIO]: `vModelRadio`,
31 [V_MODEL_CHECKBOX]: `vModelCheckbox`,
32 [V_MODEL_TEXT]: `vModelText`,
33 [V_MODEL_SELECT]: `vModelSelect`,
34 [V_MODEL_DYNAMIC]: `vModelDynamic`,
35 [V_ON_WITH_MODIFIERS]: `withModifiers`,
30const modifierGuards: Record<
31 ModifierGuards,
32 | ((e: Event) => void | boolean)
33 | ((e: Event, modifiers: string[]) => void | boolean)
34> = {
35 stop: (e: Event) => e.stopPropagation(),
36 prevent: (e: Event) => e.preventDefault(),
compiler-sfc
これは名前の通り SFC (Single File Component) に関するコンパイラです.
具体的には,<script setup>
や <style scoped>
などの機能を提供します.
多くの場合,このコンパイラは別パッケージになっているバンドラ等のツールのプラグインに呼ばれることで機能します.
有名な例としては,Vite で利用される vite-plugin-vue や,webpack で利用される vue-loader などがあります.
25function tryResolveCompiler(root?: string) {
26 const vueMeta = tryRequire('vue/package.json', root)
27 // make sure to check the version is 3+ since 2.7 now also has vue/compiler-sfc
28 if (vueMeta && vueMeta.version.split('.')[0] >= 3) {
29 return tryRequire('vue/compiler-sfc', root)
30 }
31}
8import * as _compiler from 'vue/compiler-sfc'
9
10export let compiler: typeof _compiler
11
12try {
13 // Vue 3.2.13+ ships the SFC compiler directly under the `vue` package
14 // making it no longer necessary to have @vue/compiler-sfc separately installed.
15 compiler = require('vue/compiler-sfc')
16} catch (e) {
17 try {
18 compiler = require('@vue/compiler-sfc')
19 } catch (e) {
20 throw new Error(
21 `@vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc ` +
22 `to be present in the dependency tree.`
23 )
24 }
25}
runtime-core
ランタイムのコア部分を提供します.
こちらも DOM には依存しない,コンポーネントのランタイムの実装や,仮想 DOM とそのパッチ,スケジューラの実装などが含まれます.
パッチ処理 (renderer) に関しては,実際に DOM 操作が行われそうな雰囲気がありますが,runtime-core では非 DOM API 依存に定義された interface の呼び出しのみを行っており,
実際の関数は runtime-dom に実装され,注入されています.(依存性逆転の法則を利用しています.)
interface:
108export interface RendererOptions<
109 HostNode = RendererNode,
110 HostElement = RendererElement,
111> {
112 patchProp(
113 el: HostElement,
114 key: string,
115 prevValue: any,
116 nextValue: any,
117 namespace?: ElementNamespace,
118 parentComponent?: ComponentInternalInstance | null,
119 ): void
120 insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
121 remove(el: HostNode): void
122 createElement(
123 type: string,
124 namespace?: ElementNamespace,
125 isCustomizedBuiltIn?: string,
126 vnodeProps?: (VNodeProps & { [key: string]: any }) | null,
127 ): HostElement
128 createText(text: string): HostNode
129 createComment(text: string): HostNode
130 setText(node: HostNode, text: string): void
131 setElementText(node: HostElement, text: string): void
132 parentNode(node: HostNode): HostElement | null
133 nextSibling(node: HostNode): HostNode | null
134 querySelector?(selector: string): HostElement | null
135 setScopeId?(el: HostElement, id: string): void
136 cloneNode?(node: HostNode): HostNode
137 insertStaticContent?(
138 content: string,
139 parent: HostElement,
140 anchor: HostNode | null,
141 namespace: ElementNamespace,
142 start?: HostNode | null,
143 end?: HostNode | null,
144 ): [HostNode, HostNode]
145}
createRenderer という関数が option として実際のオペレーションを受け取る(runtime-core では直接呼び出さない):
325export function createRenderer<
326 HostNode = RendererNode,
327 HostElement = RendererElement,
328>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {
runtime-dom
上記で説明したうちの,実際の DOM オペレーションの実装や,それらを core に注入する実装が含まれます.
45export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
46 insert: (child, parent, anchor) => {
47 parent.insertBefore(child, anchor || null)
48 },
49
50 remove: child => {
51 const parent = child.parentNode
52 if (parent) {
53 parent.removeChild(child)
54 }
55 },
56
57 createElement: (tag, namespace, is, props): Element => {
58 const el =
59 namespace === 'svg'
60 ? doc.createElementNS(svgNS, tag)
61 : namespace === 'mathml'
62 ? doc.createElementNS(mathmlNS, tag)
63 : is
64 ? doc.createElement(tag, { is })
65 : doc.createElement(tag)
66
67 if (tag === 'select' && props && props.multiple != null) {
68 ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
69 }
70
71 return el
72 },
73
74 createText: text => doc.createTextNode(text),
75
76 createComment: text => doc.createComment(text),
77
78 setText: (node, text) => {
79 node.nodeValue = text
80 },
81
82 setElementText: (el, text) => {
83 el.textContent = text
84 },
85
86 parentNode: node => node.parentNode as Element | null,
87
88 nextSibling: node => node.nextSibling,
89
90 querySelector: selector => doc.querySelector(selector),
他にも,compiler の説明でも触れた,実際に DOM イベントを処理するための実装なども含まれています.
(compiler-dom はこれらの呼び出しを行うコードを出力するための実装です.)
18type ModifierGuards =
19 | 'shift'
20 | 'ctrl'
21 | 'alt'
22 | 'meta'
23 | 'left'
24 | 'right'
25 | 'stop'
26 | 'prevent'
27 | 'self'
28 | 'middle'
29 | 'exact'
30const modifierGuards: Record<
31 ModifierGuards,
32 | ((e: Event) => void | boolean)
33 | ((e: Event, modifiers: string[]) => void | boolean)
34> = {
35 stop: (e: Event) => e.stopPropagation(),
36 prevent: (e: Event) => e.preventDefault(),
37 self: (e: Event) => e.target !== e.currentTarget,
38 ctrl: (e: Event) => !(e as KeyedEvent).ctrlKey,
39 shift: (e: Event) => !(e as KeyedEvent).shiftKey,
40 alt: (e: Event) => !(e as KeyedEvent).altKey,
41 meta: (e: Event) => !(e as KeyedEvent).metaKey,
42 left: (e: Event) => 'button' in e && (e as MouseEvent).button !== 0,
43 middle: (e: Event) => 'button' in e && (e as MouseEvent).button !== 1,
44 right: (e: Event) => 'button' in e && (e as MouseEvent).button !== 2,
45 exact: (e, modifiers) =>
46 systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m)),
47}
reactivity
名前の通り,Vue.js のリアクティビティシステムを提供します.
どこかで,「Vue.js のリアクティビティシステムは out of box で利用可能だ」,という話を聞いたことがあるかもしれませんが,これはこのパッケージが他のパッケージに依存せず独立して実装されているためです.
そして,この「独立している」という点も Vapor Mode の実装においては重要なポイントとなります.
それもそのはず,少しネタバレをしておくと,Vapor Mode は仮想 DOM を使わずにリアクティビティシステムを活用することで画面を更新していくわけですが,実際のところこのリアクティビティのパッケージにはほとんど変更が入っていません.
詰まるところ,Vapor の機能の一部としてスッと使えてしまうほど Vue.js のランタイムには依存していないのです.
compiler-vapor, runtime-vapor
さて,ようやく今回のメインです. 名前の通り,Vapor Mode のコンパイラとランタイムの実装です.
Vapor Mode は現在 R&D のフェーズであるため,なるべく upstream にある既存の実装には手を加えずに済むように独立したパッケージとして実装されています.
そのため,既存の runtime, compiler と大きく被る部分もありますが,実はこの部分関しても重複して実装しています.
このパッケージでどのような実装がされているかなどはこれから見ていく (というかそれがこの本のメインの話) なので,ここでは省略します.
ざっくり,パッケージの全体構成がわかったところで早速 Vapor Mode の実装を理解するために必要なソースコードを読んでいきましょう!