Preface
Vue
In addition to the responsive principle in the core, view rendering is also a top priority . We all know that every time we update data, we will go through the logic of view rendering, and the logic involved is also very cumbersome.
This article mainly analyzes the initialization view rendering process. You will learn Vue
how to build the component starting from mounting it VNode
, and how to VNode
convert it into a real node and mount it on the page.
Mount component ($mount)
Vue
Is a constructor, new
instantiated through keywords.
// 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)
}
Upon instantiation, _init
initialization is called.
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
_init
will be called within $mount
to mount the component, and $mount
the method is actually called 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) // 渲染页面函数
}
}
// 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, { // 渲染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
}
mountComponent
In addition to calling some life cycle hook functions, the most important thing is that updateComponent
it is the core method responsible for rendering the view, which has only one line of core code:
vm._update(vm._render(), hydrating)
vm._render
Create and return VNode
, vm._update
accepting VNode
conversion to a real node.
updateComponent
will be passed in 渲染Watcher
. Whenever data changes trigger Watcher
an update, this function will be executed and the view will be re-rendered. updateComponent
After being passed in 渲染Watcher
, it will be executed once for initial page rendering.
Therefore, we focus on analyzing the two methods vm._render
and vm._update
, which is also the main principle understood in this article - Vue
the view rendering process.
Build VNode(_render)
The first is _render
the method, which is used to build the component VNode
.
// src/core/instance/render.js
Vue.prototype._render = function () {
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
_render
The method will be executed internally render
and the built one will be returned VNode
. render
Generally, it is a method generated after template compilation, or it may 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)
}
initRender
During initialization, two methods will be bound to the instance, namely vm._c
and vm.$createElement
. Both of them are calling createElement
methods, which are VNode
the core methods of creation. The last parameter is used to distinguish whether it is user-defined.
vm._c
render
The application scenario is to call it in the function generated by compilation , vm.$createElement
and it is used in the scenario of user-defined render
functions. render
Just like the parameters are passed in when calling above , it is the parameter we receive vm.$createElement
in the custom function.render
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)
}
createElement
A method is actually _createElement
an encapsulation of a method, which allows the parameters passed in to be more flexible.
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()
}
}
_createElement
It will be received in the parameter children
, which represents the current VNode
child node. Because it is of any type, it needs to be standardized into a standard VNode
array next;
// 这里规范化 children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
simpleNormalizeChildren
and normalizeChildren
are used for normalization children
. Determine normalizationType
whether render
the 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
}
simpleNormalizeChildren
The method calling scenario is the render function when the function is compiled. normalizeChildren
The main method calling scenario is that the render function is handwritten by the user.
After children
normalization, children
it becomes an VNode
array of type. After that comes the creation VNode
logic.
// 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)
}
If tag
it is string
a type, then determine if it is some built-in node and create a normal one VNode
; if it is a registered component name, createComponent
create a component type VNode
; otherwise, create an unknown label VNode
.
If tag
it is not string
a type, it is Component
a type, and the node createComponent
to create a component type is directly called VNode
.
Finally , _createElement
one will be returned VNode
, which is the vm._render
one created when calling VNode
. It will then VNode
be passed to vm._update
the function to generate the real DOM.
Generate 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.
}
_update
The core method is vm.__patch__
the method. The definition of methods on different platforms __patch__
will be slightly different. 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
You can see that the method __patch__
is actually called patch
.
// 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 })
The method patch
is createPatchFunction
the function returned by the method creation.
// 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){}
}
There are two more important objects nodeOps
here modules
. nodeOps
It is an encapsulated native DOM operation method. In the process of generating the real node tree, DOM related operations are all nodeOps
methods within the call.
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 functions and ref hook functions. In patch
the stage, the corresponding type will be taken out and called based on the behavior of the operating node.
patch
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
When rendering for the first time, vm.$el
it corresponds to the root node dom object, which is the well-known div with the id app. It oldVNode
is passed in as a parameter patch
:
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
}
Determine whether it is a real node by checking its attributes nodeType
(attributes that are only available for real nodes) .oldVnode
const isRealElement = isDef(oldVnode.nodeType)
if (isRealElement) {
// ...
oldVnode = emptyNodeAt(oldVnode)
}
Obviously the first time isRealElement
is true
, so it will be called emptyNodeAt
to convert it to VNode
:
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
Then createElm
the method will be called, which is VNode
the core method that will be converted into the 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)
}
}
Initially it will be called createComponent
to try to create a node of the component type, and will be returned if successful true
. During the creation process, $mount
component-wide mounting is also called, so patch
this process is still followed.
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
If the creation is not completed, it means that the VNode
corresponding node is a real node, and the logic of creating the real node continues.
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
Create tag
a real node of the corresponding type and assign vnode.elm
it to it as the parent node container, and the created child nodes will be placed inside.
Then call to createChildren
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)))
}
}
The child node array is traversed internally, and createElm
the created node is called again, and the one created above vnode.elm
is passed in as the parent node. This cycle continues until there are no child nodes, and a text node will be created and inserted into vnode.elm
.
After the execution is completed, it will be called invokeCreateHooks
. It is responsible for the hook function when performing DOM operations create
and will be VNode
added to 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)
}
}
The last step is to call insert
the 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)
}
}
}
You can see that the node tree is created Vue
through recursive calls . createElm
It also shows that the deepest child node will call insert
the insert node first. Therefore, the insertion order of the entire node tree is "son first, then parent". insertBefore
The method of inserting nodes is the method and method of native dom appendChild
.
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
createElm
After the process is completed, the completed node tree has been inserted into the page. In fact, Vue
when initializing the rendering page, the original root node is not app
actually replaced, but a new node is inserted behind it, and then the old node is removed.
So createElm
it will be called later removeVnodes
to remove the old node, which also calls the native dom method removeChild
.
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
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])
}
}
}
At patch
the end invokeInsertHook
, the method is called to trigger the hook function for node insertion.
At this point, the entire page rendering process is complete~
Summarize
Initialize $mount
the mounted component.
_render
Start buildingVNode
, the core method iscreateElement
, generally create an ordinaryVNode
,When a component is encountered, the component type is created
VNode
, otherwise it is of unknown tagVNode
, and the construction is completed and passed to_update
.
patch
The stage is based onVNode
creating a real node tree. The core method iscreateElm
,If the component type is encountered first
VNode
, it will be executed internally$mount
, and the same process will be followed again.Ordinary node types create a real node, and if it has child nodes, start the recursive call
createElm
, usinginsert
insert child nodes, until there are no child nodes, then fill the content node.After the final recursion is completed, the entire node tree is also
insert
inserted into the page, and then the old root node is removed.