vue3になりましたね
vue2系の仮想DOMの仕組みは以下の文献でわかりやすく書いてありました(感謝😈)
Vue.jsの仮想DOMと差分レンダリングの仕組み①
Vue.jsの仮想DOMと差分レンダリングの仕組み②
Vue.jsの仮想DOMと差分レンダリングの仕組み③
vue3のソースコードもvue2と同じかなと思ったら結構違いました🙄
解説記事もあんまないし、ざっとpatch処理の初回レンダリングの箇所だけ読んで書きます
"vue": "^3.0.2-0"
runtime-core/dist/runtime-core.esm-bundler.js
しか基本的に触れないです
vue3の仮想DOM生成を図解
基本的にはvue2と同じで、コンポーネントが1つのインスタンスで、その中にvnodeのツリーになります
vue3から、ルートコンポーネントにdivがなくても動くようになったのですが
これはSymbol(Fragment)
というコンポーネントが自動でラップしてくれると思ってください
例えば
<template>
<div id="main">
<div><p>コメント1</p></div>
<hoge />
</div>
</template>
<template>
<div>コメント2</div>
<div>
<div v-if="true">コメント3</div>
<div v-else>コメント4</div>
<div v-if="false">コメント5</div>
</div>
</template>
コンポーネントが1つのインスタンス
インスタンスの中に、htmlタグの通りのvnode
各vnodeでpatchが実行されると思ってください(patchが実行される順番を番号にしました)
流れで書くと以下のような感じ
-
import App from './App'
のApp
をvnodeに変換して最初のpatch実行(番号1) -
先頭のvnode(AppのSubTree)
と子タグのvnode(div)
とvnode(hoge)
を生成して、先頭のvnode
に対してpatch実行(番号2)
コンポーネントじゃないhtmlタグのVNode生成は事前に行ってるみたいな細けぇコメント禁止🖕 -
先頭のvnode.children(divとhoge)
をそれぞれpatch実行する、html順で先にvnode(div)
のpatch実行(番号3) -
vnode(div)
のvnode.children(p)
のpatch実行(番号4) -
vnode(hoge)
のpatch実行(番号5) -
先頭のvnode(HogeのSubTree)
と子タグのvnode(div)
とvnode(div)
を生成して、先頭のvnode
に対してpatch実行(番号6) - 番号7~10は同じ流れなので省略
*v-if/v-else
はどっちか1つしかvnodeとして扱わないです
*v-if="false"
はコメントアウトのvnode(Symbol(Comment)
)として扱われるので、div
ではなくComment
って書いてあります
vue3の仮想DOM生成のソースコードを軽く説明
図と箇条書きでザッと説明しましたが、同じ内容をmethod名で追っかけます
NODE_ENV !== 'production'
の時の処理とか、hydrate
やsuspense
やcompositionAPI
用の分岐とかめっちゃありますが// 略
で飛ばします
app.mount()から最初のpatch()まで
最初にapp.mount('#main')
みたいに実行すると思いますが、
以下のmountの関数をラップしたmountが呼ばれます(基本的に以下のmountが呼ばれると思ってください)
mount(rootContainer, isHydrate) {
if (!isMounted) {
// rootComponentは import App from './App' のApp(厳密にはちょっと違います)
const vnode = createVNode(rootComponent, rootProps);
// 略
// rootContainerはindex.htmlのelement
render(vnode, rootContainer);
// 略
}
}
const render = (vnode, container) => {
// 略
patch(container._vnode || null, vnode, container);
flushPostFlushCbs(); // ここでmounted()がまとめて実行されます(道中でmountedを登録してる)
container._vnode = vnode;
}
patch関数は、そのvnodeのtypeを判定してるだけです
今回使うvnodeの種類は、ELEMENT(divとp), COMPONENT(AppとHoge), Fragment, Comment(コメントアウト)
です
関係ないやつは//略
にしました
// n1は更新時に使うので、初回レンダリングは n1 = null です
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 略
const { type, ref, shapeFlag } = n2;
switch (type) {
case Text:
// 略
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
case Static:
// 略
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else if (shapeFlag & 6 /* COMPONENT */) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
// 略(TELEPORTとSUSPENSEとエラー制御)
}
// 略
};
コンポーネントのpatch(processComponent)
番号1のpatch処理はprocessComponent
です
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 略
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
// 略
}
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// コンポーネントのインスタンス生成はここ、instanceから先頭のvnodeを生成して、patch処理を実行し始めます
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
// 略
setupComponent(instance); // propsやslotの処理,compositionAPIのsetup実行,created実行など、今回とは関係ないのでスルー
// 略
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
}
先頭のvnodeはsubTree
って呼ばれてます
このsubTree
のpatch処理実行が番号2です
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 略(beforeMount実行などしてる)
const subTree = (instance.subTree = renderComponentRoot(instance));
// 略
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
// 略(mountedを後で実行するように登録などしてる)
instance.isMounted = true;
}
else {
// 略
}
}, prodEffectOptions);
};
HTMLElementのpatch(processElement)
番号2のpatch処理はprocessElement
です(先頭のhtmlタグがdivだから)
mountChildren
の中のpatch処理実行が番号3です
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 略
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
// 略
}
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el;
let vnodeHook;
const { type, props, shapeFlag, transition, scopeId, patchFlag, dirs } = vnode;
// 略
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is); // createElementはここ
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren);
// 略(transitionのbeforeEntry実行などしてる)
hostInsert(el, container, anchor); // insertBeforeはここ
// 略
}
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]));
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
}
その他のvnodeのpatch(番号3~10)
- 番号3は、
div
のpatch処理なのでprocessElement
- 番号4は、
p
のpatch処理なのでprocessElement
- 番号5は、
Hoge
のpatch処理なのでprocessComponent
- 番号6は、
Fragment
のpatch処理なのでprocessFragment
- 番号7は、
div
のpatch処理なのでprocessElement
- 番号8は、
div
のpatch処理なのでprocessElement
- 番号9は、
div
のpatch処理なのでprocessElement
- 番号10は、
Comment
のpatch処理なのでprocessCommentNode
*processFragment
ではmountChildren
を呼ぶ等、processElement
を簡素化した感じ
*processCommentNode
ではhostInsert
しかしてないです
*どれもmountChildren
が終了してやっと親のpatch処理が終了するので、番号1と番号2のpatch処理は最後に終わります
所感
ざっくり説明ですみません(//略
で終わらせてますが、もっといっっっぱい分岐と実行があります)
Vue.jsの仮想DOMと差分レンダリングの仕組み②に書いてあるpatch関数のフローチャート
とか死んでも作りたくないと思いました🙀
vue-next
にプルリク作れるぐらい理解しないと作れないと思います
全体的に思ったのですが、ソースコードに分岐が多すぎるなあって感じ
更新と初回レンダリングの分岐や本番と開発環境の分岐は仕方ないとして、compositionAPI
とかsuspense
とか、もうちょっと整理して書く設計しなかったのかと、やや疑問
というか何で全部1ファイルにソースコードまとめてるんだろう?現代っ子だから?