How does vNode transition to the real DOM?
What is a vnode
A VNode is essentially a JavaScript object that describes the DOM. It can describe different types of nodes in vue.js, such as common element nodes, component nodes, and so on.
Common element nodes are familiar. For example, we define a button tag in HTML to write a button:
<button class="btn" style="color: blue">I'm a button</button>
Copy the code
Correspondingly, we can use vnode to represent the button tag
const vnode = {
type: 'button'.props: {
class: 'btn'.style: {
color: 'blue',}},children: 'I'm a button.'
}
Copy the code
The type attribute represents the tag type of the DOM, the props attribute represents some DOM attachment information, such as style, class, etc., and the children attribute represents a child node of the DOM, which can also be a VNode array.
What is a component
A component is an abstraction of a DOM tree.
For example, we now define a component node on the page:
<my-component></my-componnet>
Copy the code
This code does not render a my-Component tag on the page. What it renders depends on how you write the template for the MyComponent. For example, the template definition inside the MyComponent looks like this:
<template>
<div>
<h2>I'm a component</h2>
</div>
</template>
Copy the code
As you can see, the template ends up rendering a div on the page with an H2 tag inside that shows I’m a component text.
So, in terms of representation, the component’s template determines the DOM tag that the component generates, and inside vue.js, a component that actually renders the DOM needs to go through the “create VNode-render vNode-generate DOM” steps
So what does vNode have to do with components?
We now know that a VNode can be used to describe an object in the real DOM, and it can also be used to describe components. For example, we introduce a component tag custom-Component in the template:
<custom-component msg="test"></custom-component>
Copy the code
We can represent custom-component component tags with vnode as follows:
const CustomComponent = {
// Define the component object here
}
const vnode = {
type: CustomComponent,
props: {
msg: 'test'}}Copy the code
The component VNode is really a description of something abstract, because we don’t actually render a custom-Component tag on the page, but an HTML tag defined inside the component.
Core rendering process: Create and render a VNode
When vue3 initialses an app, it creates an app object. The app.mount function is created as follows:
mount(rootContainer, isHydrate, isSVG) {
// Create a vNode for the root component
const vnode = createVNode(rootComponent, rootProps)
/ / render vnode
render(vnode, rootContainer, isSVG);
}
Copy the code
Create a vnode
Vnodes are created using the function createVNode. Let’s look at the rough implementation of this function:
function createVNode(type, props = null ,children = null) {
// If the type passed in is itself a vnode, clone the vnode and return
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */);
if (children) {
// Convert children of different data types into arrays or text types
normalizeChildren(cloned, children);
}
return cloned;
}
if (props) {
// handle props logic, standardize class and style
}
// Encode vNode type information
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
const vnode = {
type,
props,
shapeFlag,
// Some other attributes
}
// Convert children of different data types into arrays or text types
normalizeChildren(vnode, children)
return vnode
}
Copy the code
CreateVNode does a simple thing: normalize props, code vnode types, createVNode objects, and standardize children.
Now that we have the VNode object, all we need to do is render it into the page.
Rendering vnode
Next, the vNode is rendered. Let’s look at the render function implementation:
const render = (vnode, container, isSVG) = > {
if (vnode == null) {
// Destroy the component
if (container._vnode) {
unmount(container._vnode, null.null.true); }}else {
// Create or update the component
patch(container._vnode || null, vnode, container, null.null.null, isSVG);
}
// Invoke the callback scheduler
flushPostFlushCbs();
// Cache the vNode node, indicating that it has been rendered
container._vnode = vnode;
};
Copy the code
The implementation of the render function is simple: if its first argument, vnode, is null, the logic to destroy the component is executed; otherwise, the logic to create or update the component is executed. We’ll ignore flushPostFlushCbs for now and look at what it does when we analyze the nextTick principle.
path vnode
Next, let’s look at the implementation of the path function that creates or updates component nodes:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false:!!!!! n2.dynamicChildren) = > {
// Same node
if (n1 === n2) {
return;
}
// If there are old and new nodes, and the types of the old and new nodes are different, the old node is destroyed
if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1); unmount(n1, parentComponent, parentSuspense,true);
n1 = null;
}
switch (type) {
case Text:
// Process text nodes
processText(n1, n2, container, anchor);
break;
case Comment$1:
// Process the comment node
processCommentNode(n1, n2, container, anchor);
break;
case Static:
// Handle static nodes
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
}
else {
patchStaticNode(n1, n2, container, isSVG);
}
break;
case Fragment:
// Process the Fragment element
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
// Handle normal DOM elements
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else if (shapeFlag & 6 /* COMPONENT */) {
// Process components
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else if (shapeFlag & 64 /* TELEPORT */) {
/ / processing TELEPORT
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
}
else if (shapeFlag & 128 /* SUSPENSE */) {
/ / deal with SUSPENSE
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
}
else {
warn$1('Invalid VNode type:', type, ` (The ${typeof type}) `); }}/ / ref
if(ref ! =null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
};
Copy the code
I’m trying to patch it up. This function has two functions:
- Mount the DOM according to the VNode
- Update the DOM based on the old and new VNodes
For the first rendering, we will only analyze the creation process here, and the update process will be analyzed in the following sections.
In the process of creation, the patch function accepts multiple parameters. Here, we only focus on the first three:
-
The first parameter n1 indicates the old vNode. When n1 is null, it indicates a mount process.
-
The second parameter, n2, represents the new vNode, and different processing logic will be executed later depending on the vNode type.
-
The third parameter container represents the DOM container. After the DOM is rendered by vNode, it is mounted under the Container.
For rendered nodes, we focus here on rendering logic for two types of nodes: processing of components and processing of ordinary DOM elements.
Path: the component
Let’s look at the implementation of the processComponent function that handles the component:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
// Mount the component
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
} else {
// Update the componentupdateComponent(n1, n2, optimized); }};Copy the code
The logic of this function is simple: if n1 is null, the logic of mounting the component is performed; otherwise, the logic of updating the component is performed.
Let’s look at the implementation of the mountComponent function that mounts the component:
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
// Create a component instance
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
if (isKeepAlive(initialVNode)) {
// Component cache
instance.ctx.renderer = internals;
}
// Set up component instances, such as props and slots
setupComponent(instance);
if (instance.asyncDep) {
// Asynchronous components
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
if(! initialVNode.el) {const placeholder = (instance.subTree = createVNode(Comment$1));
processCommentNode(null, placeholder, container, anchor);
}
return;
}
// Set up and run the render function with side effects
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
Copy the code
As you can see, the mountComponent function does three main things: create a component instance, set up a component instance, and set up and run a rendering function with side effects.
Where, if there is a component cache, the instance’s renderer is replaced. This is all you need to know for now, but we will explore this further when we examine component caching mechanisms later. Asynchronous component processing is similar, and will be examined in more detail when analyzing dynamic components later.
Creating a component instance
Let’s look at the implementation of createComponentInstance:
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// The component's context is its own if it is the root component, otherwise it inherits the parent component's context
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
vnode,
type,
parent,
appContext,
root: null.next: null.subTree: null.update: null.scope: new EffectScope(true /* detached */),
render: null.provides: parent ? parent.provides : Object.create(appContext.provides),
renderCache: [].// local resovled assets
components: null.directives: null.// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
// emit
emit: null.emitted: null.// props default value
propsDefaults: EMPTY_OBJ,
// inheritAttrs
inheritAttrs: type.inheritAttrs,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null.// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0.asyncDep: null.asyncResolved: false.// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false.isUnmounted: false.isDeactivated: false.bc: null.c: null.bm: null.m: null.bu: null.u: null.um: null.bum: null.da: null.a: null.rtg: null.rtc: null.ec: null.sp: null
};
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
// apply custom element special handling
if (vnode.ce) {
vnode.ce(instance);
}
return instance;
}
Copy the code
As you can see, an instance of a component is a JS object, which contains many properties, consistent with the abstract concept of a component we introduced earlier.
Setting up component instances
Once the component instance is created, I’ll look at the implementation of the setupComponent function:
function setupComponent(instance, isSSR = false) {
isInSSRComponentSetup = isSSR;
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
// Handle the props of the component
initProps(instance, props, isStateful, isSSR);
// Handle component slots
initSlots(instance, children);
// Handle other component properties, such as creating the render function
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
Copy the code
SetupComponent sets up component initialization data, such as props, slots, Render, and so on.
Set up and run render functions with side effects
Finally, let’s look at the implementation of running setupRenderEffect with side effects:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > {
const componentUpdateFn = () = > {
// omit the life hook
if(! instance.isMounted) {if (el && hydrateNode) {
/ / omit hydrateNode
} else {
// Render component generates subtree vNodes
const subTree = (instance.subTree = renderComponentRoot(instance))
// Mount the subtree vNode to the container
patch(null,subTree,container,anchor,instance,parentSuspense,sSVG)
// Preserve the child root DOM nodes generated by rendering
initialVNode.el = subTree.el
}
// omit the life hook
instance.isMounted = true
} else {
// Update the component}}// Create a reactive side effect rendering function
const effect = new ReactiveEffect(
componentUpdateFn,
() = > queueJob(instance.update),
instance.scope // track it in component's effect scope
)
// Manually bind this to the side effect rendering function
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
// Execute the side effect rendering function
update()
}
Copy the code
The setupRenderEffect function has so many responsibilities that we are now analyzing only the initial rendering process, omitting the other logic. Examples include component lifecycle, hydrateNode, logic for updating components, and so on.
In addition, creating reactive side effects functions can be very abstract. It is easy to understand that instantiating a ReactiveEffect produces the side effect effcet function. The side effect, which you can simply interpret as “componentEffect”, is that when the component’s data changes, the effect function is wrapped around the internal rendering function componentEffect, which re-renders the component. We will explore reactiveeffects further when we read the reactive correlation code.
The initial rendering does two things: the rendering component generates the subTree and mounts the subTree into the Container.
RenderComponentRoot executes the render function to create vNodes inside the entire component tree. RenderComponentRoot executes the render function to create vNodes inside the entire component tree. Standardizing the vNode through an internal layer gives the result that this function returns: a subtree vNode.
After rendering the subtree vNode, the next step is to call the patch function to mount the subtree vnode to the Container.
Path: Normal DOM element
After analyzing the flow of the PATH component, let’s look at the implementation of the processElement function for the path normal DOM element:
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
isSVG = isSVG || n2.type === 'svg';
if (n1 == null) {
// // Mount the element node
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
// Update the element nodepatchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); }};Copy the code
The logic is similar to the processComponent function: if n1 is null, mount element node logic, otherwise update element node logic.
Let’s look at the implementation of mountElement mountElement:
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
let el;
let vnodeHook;
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
{
// Create a DOM element node
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// Handle the case where the child node is plain text
hostSetElementText(el, vnode.children);
} else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// Handle the case where the child nodes are arrays
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type ! = ='foreignObject', slotScopeIds, optimized);
}
// Handle props, such as class, style, event, etc
if (props) {
for (const key in props) {
if(key ! = ='value' && !isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren); }}}// Handle CSS scopes
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
}
// Mount the created DOM element node to the container
hostInsert(el, container, anchor);
};
Copy the code
As you can see, mount elements do five things: create DOM element nodes, work with text or array children, work with props, work with CSS scopes, and mount DOM elements to containers.
DOM elements are created using the hostCreateElement method, a platform-specific method that in a Web environment corresponds to:
function createElement(tag, isSVG, is) {
isSVG ? document.createElementNS(svgNS, tag)
: document.createElement(tag, is ? { is } : undefined)}Copy the code
It calls the underlying DOM API Document.createElement to create the element.
Similarly, if the child node is a text node, the hostSetElementText method is executed, which sets the text in the Web environment by setting the textContent attribute of the DOM element:
function setElementText(el, text) {
el.textContent = text
}
Copy the code
Handle the props, handle the CSS scope, we won’t do the analysis right now.
To handle the case where the children are arrays, call the mountChildren method:
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, start = 0) = > {
for (let i = start; i < children.length; i++) {
// Preprocess the child node (optimize)
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]));
// Recursive patch mounts child
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); }};Copy the code
The mount logic of the child node is also very simple, traversing children to obtain each child, and then recursively mount each child by performing the patch method.
It should be noted that recursive patch is a depth-first traversal of the tree.
After all the child nodes are processed, the DOM element node is finally mounted to the Container using the hostInsert method.