The author:Xu จ ุ ๊ บ, prohibit reprinting without authorization.
preface
The last article “Vue3 source code (a)” a brief introduction to the Vue3 source code structure, but also through the source code to learn the Vue3 foundation is also the core response. This time let’s move on to another core component, learning about Vue3 component initialization and its rendering process. If there are mistakes, omissions, but also hope to correct, supplement.
The body of the
Remember the initial Vue3 application mentioned in the last article?
createApp(App).mount('#app')
Mount (‘# App ‘) createApp(App) creates and returns a specific App instance using closures and Currification for different scenarios and platforms.
The mount method
Reviewing the previous article, we found that the source code defines the mount method in two main places:
- Runtime-dom/SRC /index.ts is rewritten for the browser Web platform
mount
methods
const { mount } = app
app.mount = (containerOrSelector: Element | string): any= > {
NormallizeContainer As the name implies, the mount argument may be a DOM object or a selector
// Select the corresponding DOM if it is a selector
const container = normalizeContainer(containerOrSelector)
if(! container)return
// App._component is the packaged and compiled app component we pass in with the rootComponent argument (Figure 1)
const component = app._component
// If we pass in a component that does not define render and does not have a template, use the original content in the DOM as the template
if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML }// This will clear the original content in the DOM
container.innerHTML = ' '
// Execute the previously temporary base mount method
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app'.' ')
return proxy
}
Copy the code
Figure 1:
Through the code and comments inside, you can divide the rewrite method into several steps: 1. 2. Determine the incoming root component App; 3. Perform the standard mount method.
- Runtime-core/SRC/apicreateapp. ts, which is a standard, cross-platform component in an app instance
mount
methods
mount(rootContainer: HostElement, isHydrate? : boolean): any {// Whether the app has been mounted
if(! isMounted) {Create VNode rootComponent = App component passed in createApp(App
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// App instance storage context, mainly app instance itself, various Settings, configuration items
vnode.appContext = context
if (isHydrate && hydrate) {
// Server render related
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 2. render VNode
// The "render" here is one that is created with one ensureRenderer mentioned in the previous article
render(vnode, rootContainer)
}
isMounted = true
// Store the DOM container
app._container = rootContainer
// for devtools and telemetry; (rootContaineras any).__vue_app__ = app
// ...
returnvnode.component! .proxy }else if (__DEV__) {
// ...}},Copy the code
The standard mount method is as follows: 1. Create a VNode. 2. Render the VNode as a real DOM
summary
At this point, we know roughly what the mount method does.
- NormalizeContainer gets the DOM container
- CreateVNode, creates a VNode based on the incoming App component
- Render VNode and mount it to the DOM container
- Returns the proxy for vnode.ponent
Let’s move on to VNode.
Create & render vNodes
I believe that you are familiar with VNode, simply described through JavaScript objects abstract DOM, things. When asked about the benefits of the interview, you will probably mention these: 1. Don’t change the DOM as often, 2. 3. Performance advantages of VNode operating JS over DOM directly. However, after reading some articles recently, I believe that the third advantage is not absolute. For components with a large amount of data, such as Tree and Table, it takes a long time to iterate through the render sub-vNode, and DOM operation is still necessary in the end, and the page can even feel the lag.
Let’s go back to the example below
App.vue
<template>
<HelloWorld msg=Vue 3.0 + Vite />
<p>{{ showText }}</p>
</template>
HelloWorld.vue
<template>
<div>{{ msg }}</div>
</template>
Copy the code
Create a VNode
In Vue3, there are many vNodes that represent different categories, such as the HelloWorld component VNode in the example above, and the common element VNode P.
In particular, let’s look at the createVNode method, which generates vNodes. The code is slightly longer, and the old method comments out the content that the process does not care about.
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
) :VNode {
if(! type || type === NULL_DYNAMIC_COMPONENT) { type = Comment }if (isVNode(type)) { // Clone the VNode, which is determined by the __v_isVNode attribute of type
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// class component normalization.
if (isClassComponent(type)) { / / class components
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// ...
}
// Add an encoding identifier to the component type
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 1 dom element
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE // New components in Suspense Vue3
: isTeleport(type)
? ShapeFlags.TELEPORT // 64 teleport is also new to VUe3
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 4 Status components
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT // 2 function components
: 0
// ...
const vnode: VNode = {
__v_isVNode: true,
[ReactiveFlags.SKIP]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null.component: null,
shapeFlag
// ...
}
Type 8: text; 16: array; 32: slots; It also converts to the corresponding type. * The shapeFlag of VNode will be modified to use **/ for later mounting
normalizeChildren(vnode, children)
// normalize suspense children
/ /...
return vnode
}
Copy the code
Take a look at the above code execution process through this example
- Check whether it is a VNode, Class component. If it is a Class component, perform the Class and style standardized conversion
- Determine the component type and calculate the identifier, resulting in 4
- Create a VNode
- Standard child node, where children is null when the App component is passed in
- Returns the VNode
Here we have the VNode created by the App component:
Rendering VNode
Then let’s look at render(vNode, rootContainer), how to render vNode.
Last time we looked at the Render method, baseCreateRenderer generates render for different platforms by passing in endererOptions for different platforms.
render
// runtime-core/src/renderer.ts
const render: RootRenderFunction = (vnode, container) = > {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null.null.true)}}else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
// Store vNodes in the DOM container
container._vnode = vnode
}
Copy the code
You can see that if the VNode passed in is empty and the current DOM container has a VNode, unmount the component to destroy it, otherwise patch the VNode passed in. Then we understand the implementation of patch.
patch
const patch: PatchFn = (
n1, // n1 represents the old node
n2, // n2 represents the new node
container,
anchor = null,
parentComponent = null,parentSuspense = null,isSVG = false,optimized = false
) = > {
// If there is an old VNode and it is different, umount destroys the old node
if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
// Select method by type
switch (type) {
case Text:
/ / text
processText(n1, n2, container, anchor)
break
case Comment:
/ / comment
processCommentNode(n1, n2, container, anchor)
break
case Static:
/ / static
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
// Fragmentation, which is Vue3's new support for multiple nodes
processFragment(/ * *... * * /)
break
default:
// If no type is specified, use shapeFlag
if (shapeFlag & ShapeFlags.ELEMENT) {
/ / dom elements
processElement(/ * *... * * /)}else if (shapeFlag & ShapeFlags.COMPONENT) {
// This is where the component will go for its first rendering
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
// There are two new components in Vue3
} else if (shapeFlag & ShapeFlags.TELEPORT) {
//
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
}
}
// set ref
if(ref ! =null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
Copy the code
In fact, the most important logic of patch is to choose how to deal with components through vNode type and shapeFlag.
Since we are rendering for the first time, n1 is empty, and the App component created VNode with shapeFlags. STATEFUL_COMPONENT 4, we will go to the ShapeFlags.COMPONENT condition. Execute the processComponent method. So let’s take a look at this method.
processComponent
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) = > {
if (n1 == null) {
// If there is no old node
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { / / 512
// If it is a keep-alive component; (parentComponent! .ctxas KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// Execute the mount component
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// If n1 and n2 are present, perform the update
updateComponent(n1, n2, optimized)
}
}
Copy the code
The main logic of this method is to mount the component mountComponent, or to update the component with updateComponent.
Let’s look at the mountComponent to which the initial rendering is executed
mountComponent
const mountComponent: MountComponentFn = (
initialVNode, // The initial VNode is the VNode generated by the App component
container, // #app Dom container
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) = > {
// Create a component instance
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// inject renderer internals for keepAlive
if(isKeepAlive(initialVNode)) { ; (instance.ctxas KeepAliveContext).renderer = internals
}
// Set instance initialization props, slots, and Vue3's new Composition API
setupComponent(instance)
// ...
// Effect is the side effect function mentioned in the previous article
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
Copy the code
The main logic for mounting a VNode component is createComponentInstance to create an instance of the component, setupComponent to set the component, and setupRenderEffect to perform a side effect rendering function.
CreateComponentInstance basically creates and returns instance instances, so let’s look at what instance looks like.
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
root: null! .// to be immediately set
next: null.subTree: null! .// will be set synchronously right after creation
update: null! .// will be set synchronously right after creation
render: null.proxy: null.withProxy: null.effects: null.provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null! , renderCache: [],// local resovled assets
components: null.directives: null.// resolved props and emits options
//
// emit
emit: null as any, // to be set immediately
emitted: null.// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
// ...
// suspense related
// ...
// lifecycle hooks
// The following are the attributes related to the component lifecycle
isMounted: false.isUnmounted: false.isDeactivated: false.bc: null.// beforeCreate
c: null.// created
// ...
}
Copy the code
The setupComponent method is also used to initialize the properties of instance, such as props, slots, and Vue3’s new setup function.
Because Vue3’s new Composition API and setup functions are involved, you can learn this separately
Once the instance is created and setup, the last step is to run the Render side effect function setupRenderEffect.
setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > {
// Create a reactive side effect render function
instance.update = effect(function componentEffect() {
if(! instance.isMounted) {let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance // Life cycle, beforeMounted, mounted
// bm lifecycle and hook execution
if (bm) {
invokeArrayFns(bm)
}
// ..
// Render component generates subTree VNode
const subTree = (instance.subTree = renderComponentRoot(instance))
if (el && hydrateNode) {
// ...
} else {
// Mount subTree into Dom container
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
// The lifecycle is mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// ...
instance.isMounted = true
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
}
}, prodEffectOptions)
}
Copy the code
Review the content of the last article, the effect function must be familiar, run componentEffect trigger dependency collection, collect this effect function, when the component data changes, will re-execute the effect function componentEffect method.
ComponentEffect main logic is to generate a subTree VNode, and then mount the subTree.
renderComponentRoot
export function renderComponentRoot(
instance: ComponentInternalInstance
) :VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render, // render is the.vue render function
renderCache,
data,
setupState,
ctx
} = instance
let result
currentRenderingInstance = instance
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
const proxyToUse = withProxy || proxy
// In this example, Helloworld is looping, p tag VNoderesult = normalizeVNode( render! .call( proxyToUse, proxyToUse! , renderCache, props, setupState, data, ctx ) ) fallthroughAttrs = attrs }else {
// functional
} catch (err) {
// ...
}
currentRenderingInstance = null
return result
}
Copy the code
What is a subTree? For example, in the initial example, App component is initialVNode, subTree is VNode generated by the structure of App component template, children attribute is HelloWorld component VNode, and P tag VNode.
In the chidren of the App component initialVNode, the VNode generated according to the HelloWorld tag is initialVNode for the internal DOM structure of the HelloWorld component. The VNodes generated by the internal DOM structure are subtrees.
The following is the compiled render function for helloWorld.vue in this example
This is the App subTree
As you can see, children has Helloworld, p tag VNode.
After returning to the setupRenderEffect method and generating subTree, we will go back to our previous patch process to determine how to process the incoming VNodes. The cycle will continue until the patch real DOM elements, annotations and other VNodes.
I don’t know if you’ve noticed, but in the original example, the app. vue template doesn’t have a root node, which is a new feature in Vue3. In Vue2, you definitely need a div to enclose the HelloWorld, P tag.
So the APP component subTree in our example is resolved as a VNode with type Symbol(Fragment).
Go back to the Patch method and look at processFragment
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) = > {
// There is no root node
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '))!
// ...
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// Children must be an array
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else{}}Copy the code
HostCreateText, hostInsert, and rendererOptions are props for creating render. For example hostCreateText is the document. The createTextNode hostInsert is the parent. The insertBefore (anchor child *, * * * | | null).
After processFragment determines the location, it executes mountChildren to process the Children VNode array.
mountChildren
const mountChildren: MountChildrenFn = (
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] as VNode)
: normalizeVNode(children[i]))
// Patch each VNode
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
Copy the code
MountChildren traverses children and patches each VNode to the current container.
Back to patch, let’s see how it works if it’s a DOM node VNode.
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) = > {
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
// }}Copy the code
Similar to the process of dealing with components, mount or update is determined by whether there are old nodes.
mountElement
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) = > {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const {
type,
props,
shapeFlag,
transition,
scopeId,
patchFlag,
dirs
} = vnode
// ...
// Call the API passed in to create the DOM element
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { / / 8
// Create text if it is child node text
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { / / 16
// If it is an array, go back to mountChildren to iterate over the patch child node
// Notice that the container passed in here is already the newly created EL DOM element, thus creating the parent-child relationship
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null, parentComponent, parentSuspense, isSVG && type ! = ='foreignObject', optimized || !! vnode.dynamicChildren ) }if (dirs) {
// Call instruction related lifecycle processing
invokeDirectiveHook(vnode, null, parentComponent, 'created')}// If there are PROPS for DOM, such as native class style, custom prop, etc
if (props) {
for (const key in props) {
if(! isReservedProp(key)) { hostPatchProp( el, key,null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// ...
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')}/** Mount the created EL DOM to the contanier container ** First renders the container as the #app container, but then the corresponding parent DOM container **/
hostInsert(el, container, anchor)
// ...
}
Copy the code
As you can see, the main logic for mounting a DOM node is to create the DOM by calling hostCreateElement, which is essentially the browser’s document.createElement. Then determine whether the child nodes are text or arrays. We then deal with the native or custom properties of the DOM. Finally, insert is called to mount to the DOM container.
Take the internal div of the HelloWorld component for example. Its children is just a piece of text we passed in through prop, so call hostSetElementText: el.textContent = *text* to insert the text.
One might wonder why div VNode’s shapeFlag is 9. Remember the normalizeChildren operation in the createVNode method? It changes the value of shapeFlag depending on whether children are of type array, text, or slot.
summary
Look at the code and see if the rendering process feels very convoluted, you can use the flow chart to understand it.
At the end
Thank you for reading. Recently, the big front end team of ZHicyun Health is participating in the nuggets popular team contest. If you think it’s good, then come and vote for us!
A total of 12 votes can be cast today, 4 votes can be cast on web, 4 votes can be cast on app. 4 votes can be cast on share. Thanks for your support, we will create more technical articles in 2021 ~~~
Your support is our biggest motivation ~