Magren

Magren

Idealist & Garbage maker 🛸
twitter
jike

Vue renderの落とし穴を一度記録する

前言#

数日前、自分の玩具を作っているときに、Element plus の Message コンポーネントのようなものを実現したくなり、同様にメソッドを通じてコンポーネントを呼び出すことにしました。そのために render💻を使用しましたが、コンポーネントを破棄する際に少し問題が発生しました……

思路#

  • Message コンポーネントは、transition タグを使用してそのマウントおよび破棄アニメーションを制御します
  • message.ts では、createVNode を使用して VNode を作成し、render メソッドを使用して指定された div にマウントします
  • setTimeout でカウントダウンして Message コンポーネントを破棄します
import Message from './base-message.vue'
import { createVNode, render } from 'vue'

// DOMコンテナ
const div = document.createElement('div')// DOMコンテナノードdivを作成
document.body.appendChild(div) // コンテナをbodyに追加
div.setAttribute('id', 'message-container') // DOMに一意の識別子を追加

let isShow = false

let timer: number  // タイマー識別子
export default ( type:string, message:string ) => {
  if(isShow){
    return
  }
  const vnode = createVNode(Message, { type, message})
  render(vnode, div) // 仮想DOMをdivコンテナに追加
  
  isShow = true 

  clearTimeout(timer)
  timer = setTimeout(() => { // DOMノードを削除
    isShow = false
    const child = document.getElementById('base-message') // IDに基づいてmessageコンポーネントを取得
    if(child){
      div.removeChild(child) // messageコンポーネントを削除
    }

  }, 3000)
}

この時、問題が発生しました。最初のレンダリング時に Message コンポーネントは確かに表示され、3 秒後に対応する DOM も削除されましたが、その後はこの message コンポーネントがレンダリングされなくなりました🥲

过程#

render からブレークポイントを設定すると、Vue が提供する render メソッドが見えます。container は私たちが渡したマウントする div で、vnode は createVNode を使用して作成した仮想 DOM です。この時、コードが patch メソッドに進んでいることも確認できます。

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPreFlushCbs()
  flushPostFlushCbs()
  container._vnode = vnode
}

patch コードを少し見てみると、VNode の type を判断し、ブレークポイントを設定すると、私たちのコードが processComponent メソッドに進んでいることがわかり、関連するパラメータが渡されています。

const patch: PatchFn = (
  n1, // 古い仮想ノード
  n2, // 新しい仮想ノード
  container,
  anchor = null, // ノードを挿入するためのアンカーポイントDOM
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // diff最適化を有効にするかどうか
) => {
  // 新旧仮想ノードが同じ場合、直接返す
  if (n1 === n2) {
    return
  }

  // patching & not same type, unmount old tree
  // 新旧仮想ノードが異なる場合(keyとtypeが異なる)、古い仮想ノードとその子ノードをアンマウント
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1)
    // 古い仮想ノードとその子ノードをアンマウント
    unmount(n1, parentComponent, parentSuspense, true)
    // 古い仮想ノードをnullに設定し、後でノードのマウントロジックを実行
    n1 = null
  }

  // PatchFlags.BAILフラグはdiff最適化を終了することを示します
  if (n2.patchFlag === PatchFlags.BAIL) {
    // optimizedをfalseに設定し、後続のDiffプロセスでdiff最適化を無効にします
    optimized = false
    // 新しい仮想ノードの動的子ノード配列をnullに設定し、diff最適化を無効にします
    n2.dynamicChildren = null
  }

  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text: // テキストを処理
      processText(n1, n2, container, anchor)
      break
    case Comment: // コメントを処理
      processCommentNode(n1, n2, container, anchor)
      break
    case Static: // 静的ノードを処理
      if (n1 == null) {
        // 静的ノードをマウント
        mountStaticNode(n2, container, anchor, isSVG)
      } else if (__DEV__) {
        // 静的ノードを更新
        patchStaticNode(n1, n2, container, isSVG)
      }
      break
    case Fragment: // Fragment要素を処理
      processFragment(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // ELEMENTタイプのDOM要素を処理
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // コンポーネントを処理
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // Teleportコンポーネントを処理
        // Teleportコンポーネント内部のprocess関数を呼び出し、Teleportコンポーネントの内容をレンダリング
        ;(type as typeof TeleportImpl).process(
          n1 as TeleportVNode,
          n2 as TeleportVNode,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        // Suspenseコンポーネントを処理
        // Suspenseコンポーネント内部のprocess関数を呼び出し、Suspenseコンポーネントの内容をレンダリング
        ;(type as typeof SuspenseImpl).process(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized,
          internals
        )
      } else if (__DEV__) {
        warn('無効なVNodeタイプ:', type, `(${typeof type})`)
      }
  }

  // refを設定
  // ref参照を設定
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

このメソッド内で、n1 は私たちがマウントする div 内の VNode です。これは、最初のコンポーネントのマウントと後続のマウントの違いでもあります。最初のコンポーネントをマウントする際、n1 は null ですが、その後コンポーネントを削除しても、内部の VNode は削除されていないため、直接 updateComponent メソッドに進んでしまいます。updateComponent メソッド内では、新旧仮想ノードの属性、ディレクティブ、子ノードなどに基づいてコンポーネントの更新が必要かどうかを判断しますが、どのようにしても新しい message コンポーネントをマウントすることには程遠いです😶‍🌫️

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // 初回マウント時に現在マウントするコンポーネントがKeepAliveコンポーネントかどうかを判断
          // コンポーネントをアクティブ化し、隠されたコンテナから元のコンテナに移動
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // KeepAliveコンポーネントでない場合、mountComponentを呼び出してコンポーネントをマウント
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

最后#

renderメソッド内でも見えるように、仮想 DOM をアンマウントする必要がある場合、vnode パラメータを null に設定するだけで、仮想 DOM がリアル DOM を更新し、message コンポーネントをアンマウントできます。リアル DOM を直接削除するのではなく。

import Message from './base-message.vue'
import { createVNode, render } from 'vue'

// DOMコンテナ
const div = document.createElement('div')// DOMコンテナノードdivを作成
document.body.appendChild(div) // コンテナをbodyに追加
div.setAttribute('id', 'message-container') // DOMに一意の識別子を追加

let isShow = false

let timer: number  // タイマー識別子
export default ( type:string, message:string ) => {

  if(isShow){
    return
  }

  const vnode = createVNode(Message, { type, message})
  render(vnode, div) // 仮想DOMをdivコンテナに追加
  isShow = true 
  clearTimeout(timer)
  timer = setTimeout(() => { // DOMノードを削除
    isShow = false
    render(null, div)
  }, 3000)
}

この問題は大した問題ではなく、Element plus の Message コンポーネントを参考にすれば問題がどこにあるかを発見し、直接修正することもできますが、自分自身でこのプロセスを試行錯誤するのが好きで、その後に明確な結果と認識を得ることができるのがとても嬉しいです😌

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。