Preface#
A few days ago, while making my own toy, I wanted to implement a Message component similar to Element Plus, which is also triggered by a method. So I used render💻, but encountered a small problem when destroying the component...
Idea#
- Message component, using the transition tag to control its mounting and destruction animations
- message.ts, using createVNode to create a VNode and using the render method to mount it to a specified div
- setTimeout countdown to destroy the Message component
import Message from './base-message.vue'
import { createVNode, render } from 'vue'
// DOM container
const div = document.createElement('div')// Create a DOM container node div
document.body.appendChild(div) // Append the container to the body
div.setAttribute('id', 'message-container') // Add a unique identifier class to the DOM
let isShow = false
let timer: number // Timer identifier
export default ( type:string, message:string ) => {
if(isShow){
return
}
const vnode = createVNode(Message, { type, message})
render(vnode, div) // Add the virtual DOM to the div container
isShow = true
clearTimeout(timer)
timer = setTimeout(() => { // Remove the DOM node
isShow = false
const child = document.getElementById('base-message') // Get the message component by id
if(child){
div.removeChild(child) // Remove the message component
}
}, 3000)
}
At this point, the problem arose. The Message component did indeed display during the first render, and it was indeed removed after 3 seconds, but it could not be rendered again afterwards🥲
Process#
Starting from the render breakpoint, we can see the render method provided by Vue, where the container is the div we want to mount, and vnode is the virtual DOM we created using createVNode. We can also see that the code has entered the patch method.
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
}
After looking at the patch code, based on the type of VNode and the breakpoint, we can see that our code has entered the processComponent method and passed the relevant parameters.
const patch: PatchFn = (
n1, // Old virtual node
n2, // New virtual node
container,
anchor = null, // Anchor DOM for inserting nodes before the anchor
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // Whether to enable diff optimization
) => {
// If the old and new virtual nodes are the same, return directly without diff comparison
if (n1 === n2) {
return
}
// If the old and new virtual nodes are different (key and type are different), unmount the old virtual node and its children
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
// Unmount the old virtual node and its children
unmount(n1, parentComponent, parentSuspense, true)
// Set the old virtual node to null to ensure the entire node's mount logic is followed
n1 = null
}
// PatchFlags.BAIL flag indicates that diff optimization should be exited
if (n2.patchFlag === PatchFlags.BAIL) {
// Set optimized to false, diff optimization will not be enabled in the subsequent Diff process
optimized = false
// Set the new virtual node's dynamic children array to null, so diff optimization will not be performed
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: // Handle text
processText(n1, n2, container, anchor)
break
case Comment: // Handle comments
processCommentNode(n1, n2, container, anchor)
break
case Static: // Handle static nodes
if (n1 == null) {
// Mount static nodes
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
// Update static nodes
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment: // Handle Fragment elements
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// Handle ELEMENT type DOM elements
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// Handle components
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// Handle Teleport components
// Call the process function inside the Teleport component to render the content of the Teleport component
;(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) {
// Handle Suspense components
// Call the process function inside the Suspense component to render the content of the Suspense component
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// Set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
In this method, n1 is the VNode inside the div we want to mount. This is also the difference between the first mount and subsequent mounts. When the component is first mounted, n1 is null. Although I deleted the component later, I only removed the real DOM directly and did not delete the VNode inside, which led to the direct execution of the updateComponent method, i.e., updating the component. In the updateComponent method, it will determine whether to update the component based on the properties, directives, child nodes, etc., of the new and old virtual nodes, but no matter what, it is far from what I want, which is to mount a new message component😶🌫️
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) { // Check if the component to be mounted is a KeepAlive component during the first mount
// Activate the component, moving it from the hidden container back to the original container
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// If not a KeepAlive component, call mountComponent to mount the component
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
Conclusion#
According to the render method, we can also see that if we need to unmount the virtual DOM, we just need to pass null as the vnode parameter and let the virtual DOM update the real DOM to unmount our message component, rather than directly deleting the real DOM.
import Message from './base-message.vue'
import { createVNode, render } from 'vue'
// DOM container
const div = document.createElement('div')// Create a DOM container node div
document.body.appendChild(div) // Append the container to the body
div.setAttribute('id', 'message-container') // Add a unique identifier class to the DOM
let isShow = false
let timer: number // Timer identifier
export default ( type:string, message:string ) => {
if(isShow){
return
}
const vnode = createVNode(Message, { type, message})
render(vnode, div) // Add the virtual DOM to the div container
isShow = true
clearTimeout(timer)
timer = setTimeout(() => { // Remove the DOM node
isShow = false
render(null, div)
}, 3000)
}
Although this problem is not a big issue, directly referencing Element Plus's Message component can also reveal where the problem lies and make direct modifications. However, I still enjoy the process of tinkering with it, and afterwards, I can have a clearer understanding and result😌