8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Linkbal(リンクバル)Advent Calendar 2020

Day 2

【vue3】仮想DOM生成からmountedまで(patch処理)

Last updated at Posted at 2020-12-02

vue3になりましたね
vue2系の仮想DOMの仕組みは以下の文献でわかりやすく書いてありました(感謝😈)
Vue.jsの仮想DOMと差分レンダリングの仕組み①
Vue.jsの仮想DOMと差分レンダリングの仕組み②
Vue.jsの仮想DOMと差分レンダリングの仕組み③

vue3のソースコードもvue2と同じかなと思ったら結構違いました🙄
解説記事もあんまないし、ざっとpatch処理の初回レンダリングの箇所だけ読んで書きます

package.json
"vue": "^3.0.2-0"

runtime-core/dist/runtime-core.esm-bundler.jsしか基本的に触れないです

vue3の仮想DOM生成を図解

基本的にはvue2と同じで、コンポーネントが1つのインスタンスで、その中にvnodeのツリーになります
vue3から、ルートコンポーネントにdivがなくても動くようになったのですが
これはSymbol(Fragment)というコンポーネントが自動でラップしてくれると思ってください

例えば

App.vue
<template>
  <div id="main">
    <div><p>コメント1</p></div>
    <hoge />
  </div>
</template>
hoge.vue
<template>
  <div>コメント2</div>
  <div>
    <div v-if="true">コメント3</div>
    <div v-else>コメント4</div>
    <div v-if="false">コメント5</div>
  </div>
</template>

domツリーは↓こんな感じになります
image.png

コンポーネントが1つのインスタンス
インスタンスの中に、htmlタグの通りのvnode
各vnodeでpatchが実行されると思ってください(patchが実行される順番を番号にしました)

流れで書くと以下のような感じ

  1. import App from './App'Appをvnodeに変換して最初のpatch実行(番号1)
  2. 先頭のvnode(AppのSubTree)と子タグのvnode(div)vnode(hoge)を生成して、先頭のvnodeに対してpatch実行(番号2)
    コンポーネントじゃないhtmlタグのVNode生成は事前に行ってるみたいな細けぇコメント禁止🖕
  3. 先頭のvnode.children(divとhoge)をそれぞれpatch実行する、html順で先にvnode(div)のpatch実行(番号3)
  4. vnode(div)vnode.children(p)のpatch実行(番号4)
  5. vnode(hoge)のpatch実行(番号5)
  6. 先頭のvnode(HogeのSubTree)と子タグのvnode(div)vnode(div)を生成して、先頭のvnodeに対してpatch実行(番号6)
  7. 番号7~10は同じ流れなので省略
    *v-if/v-elseはどっちか1つしかvnodeとして扱わないです
    *v-if="false"はコメントアウトのvnode(Symbol(Comment))として扱われるので、divではなくCommentって書いてあります

vue3の仮想DOM生成のソースコードを軽く説明

図と箇条書きでザッと説明しましたが、同じ内容をmethod名で追っかけます
NODE_ENV !== 'production'の時の処理とか、hydratesuspensecompositionAPI用の分岐とかめっちゃありますが// 略で飛ばします

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ファイルにソースコードまとめてるんだろう?現代っ子だから?

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?