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最適化を有効にするかどうか
) => {
  // 新旧仮想ノードが同じ場合、直接戻り、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 です。後でコンポーネントを削除したとしても、それは実際の DOM を直接削除しただけで、内部の VNode は削除されていません。これにより、直接 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 コンポーネントを参考にすれば問題がどこにあるかを見つけて直接修正できますが、自分自身はこのプロセスを試行錯誤するのが好きで、その後に比較的明確な結果と認識を得ることができるのが嬉しいです😌

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