preface
In addition to the responsive principle, view rendering is also a top priority in the Vue core. We all know that every time we update data, we will follow the logic of view rendering, and the logic involved in this is very complicated.
This article focuses on the initialization view rendering process, and you will see how Vue builds vNodes, starting with mounting components, and how vNodes are converted into real nodes and mounted to the page.
Mount Components ($mount)
Vue is a constructor, instantiated with the new keyword.
// src/core/instance/index.js
function Vue (options) {
if(process.env.NODE_ENV ! = ='production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}
Copy the code
At instantiation time, _init is called for initialization.
// src/core/instance/init.js
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Copy the code
$mount is called in _init to mount the component. The $mount method actually calls mountComponent.
// src/core/instance/lifecycle.js
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
// ...
} else {
updateComponent = () = > {
vm._update(vm._render(), hydrating) // Render the page function}}// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, { / / render watcher
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')}return vm
}
Copy the code
MountComponent calls some lifecycle hook functions, most notably updateComponent, which is the core method responsible for rendering the view. It has only one line of core code:
vm._update(vm._render(), hydrating)
Copy the code
Vm. _render creates and returns VNode, and vm._update accepts VNode to turn it into a real node.
UpdateComponent is passed into the render Watcher and is executed whenever a data change triggers a Watcher update to re-render the view. UpdateComponent is executed once after passing in the render Watcher to initialize the page rendering.
So we focus on the analysis of vm._render and vm._update methods, which is the main principle of this article — Vue view rendering process.
Build VNode (and _render)
The first is the _render method, which is used to build the component’s VNode.
// src/core/instance/render.js
Vue.prototype._render = function () {
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
Copy the code
_render internally executes the render method and returns the built VNode. Render is usually a method generated after a template is compiled, but it can also be user-defined.
// src/core/instance/render.js
export function initRender (vm) {
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)}Copy the code
InitRender on initialization will bind two methods to the instance, vm._c and vm.$createElement. Both call the createElement method, which is the core method for creating a VNode, and the last parameter is used to distinguish whether it is user-defined or not.
The vm._c application scenario is called in the compiled render function, and the vm.$createElement is used in the user-defined render function scenario. Just like the previous render call passed in the parameter vm.$createElement, this is what we receive in our custom render function.
createElement
// src/core/vdom/create-elemenet.js
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
The createElement method is essentially a wrapper around the _createElement method, allowing more flexibility in the parameters passed in.
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if(! tag) {// in case of component :is set to falsy value
return createEmptyVNode()
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
Copy the code
The _createElement argument accepts children, which represents the children of the current VNode. Since it is of any type, we need to normalize it as a standard VNode array.
// normalize children here
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
Copy the code
SimpleNormalizeChildren and normalizeChildren are both used to normalizeChildren. NormalizationType determines whether the Render function is compiled or user-defined.
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
Copy the code
The simpleNormalizeChildren method call scenario is the Render function when the function is compiled. The normalizeChildren method is invoked in scenarios where the render function is handwritten by the user.
After normalizing children, children becomes an array of type VNode. After that comes the logic to create a VNode.
// src/core/vdom/patch.js
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined.undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
Copy the code
If the tag is a string, create a normal VNode if the tag is a string. If it is a registered component name, create a VNode of the component type using createComponent. Otherwise, create a VNode with an unknown label.
If the tag is not a string, it is a Component, and a VNode of Component type is created by calling createComponent directly.
Finally, _createElement returns a VNode, which was created when vm._render was called. VNode then passes to the vm._update function, which generates the real DOM.
Generating the real DOM (_update)
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
Copy the code
__patch__ is defined slightly differently from platform to platform. In the Web platform, it is defined like this:
// src/platforms/web/runtime/index.js
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
You can see that __Patch__ actually calls the patch method.
// src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
The Patch method is a function created and returned by the createPatchFunction method.
// src/core/vdom/patch.js
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly){}}Copy the code
There are two more important objects: nodeOps and Modules. NodeOps is an encapsulated native DOM manipulation method. In the process of generating a real node tree, DOM-related operations are all called methods in nodeOps.
Modules is the hook function to be executed. When entering the function, the hook functions of different modules will be classified into CBS, including custom instruction hook function and REF hook function. In the patch phase, the corresponding type will be extracted and called according to the behavior of the operation node.
patch
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code
For the first rendering, vm.$el corresponds to the root dom object, known as the DIV with the ID app. It is passed to patch as an oldVNode argument:
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if(process.env.NODE_ENV ! = ='production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.')}}// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
/ / # 6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
Copy the code
Check whether oldVnode is a real node by checking the nodeType attribute (which is only available to real nodes).
const isRealElement = isDef(oldVnode.nodeType)
if (isRealElement) {
// ...
oldVnode = emptyNodeAt(oldVnode)
}
Copy the code
It is obvious that the first isRealElement is true, so emptyNodeAt is called to convert it to VNode:
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
Copy the code
The createElm method is then called, which is the core method for converting a VNode into a real DOM:
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = ! nested// for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if(process.env.NODE_ENV ! = ='production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
Copy the code
CreateComponent is initially called to attempt to create a node of the component type, returning true if successful. During the creation process, $mount is also called for component-wide mount, so the patch process is still followed.
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
Copy the code
If the VNode is not created, it indicates that the VNode corresponds to a real node. Go to the next step to create a real node.
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
Copy the code
Create a real node of the type according to the tag and assign it to vnode.elm, which acts as the parent node container into which the created children will be placed.
Then call createChildren to create child nodes:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
Copy the code
The child node array is iterated internally, and createElm is called again to create the node, while vNode. elm is passed in as the parent. This loop continues until there are no children, and a text node is created and inserted into vNode.elm.
InsertedVnodeQueue = insertedVnodeQueue; insertedVnodeQueue = insertedVnodeQueue; insertedVnodeQueue = insertedVnodeQueue; insertedVnodeQueue = insertedVnodeQueue;
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
Copy the code
The final step is to call the insert method to insert the node into the parent node:
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
Copy the code
You can see that Vue creates the node tree by recursively calling createElm. It also indicates that the deepest child node is inserted first with an INSERT call. Therefore, the insertion order of the entire node tree is “child before parent”. The insert node methods are the native DOM methods insertBefore and appendChild.
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0.0)}Copy the code
After the createElm process is complete, the completed node tree is inserted into the page. When Vue initializes the render page, it does not actually replace the original root node app, but inserts a new node after it, and then removes the old node.
So after createElm, removeVnodes is called to remove the old nodes, which also calls the native DOM method removeChild.
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
Copy the code
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
Copy the code
At the end of patch, invokeInsertHook method is invoked to trigger the hook function of node insertion.
At this point the entire page rendering process is completed ~
conclusion
Initialize the call to mount the component with $mount.
_render starts building a VNode. The core method is createElement. It creates a normal VNode, a component-type VNode when it encounters a component, or a VNode with an unknown tag.
In the patch phase, create a real node tree based on the VNode. The core method is createElm. When the VNode of the component type is encountered, $mount is executed and the same process is repeated. The normal node type creates a real node if it has children and starts a recursive call to createElm, using INSERT to insert the children until the content node is populated without any children. At the end of the recursion, insert is also used to insert the entire tree of nodes into the page and remove the old root node.
Previous related articles:
Touch your hand to understand the Vue responsive principle
Hand touching takes you to understand the Computed principles of Vue
Touch your hand to understand the principle of Vue Watch
Vue you have to know about the asynchronous update mechanism and the nextTick principle