Vue.js source code learning – data driven
One of the core ideas of Vue.js is data-driven. Data-driven means that views are generated by data drivers, and views are modified not by manipulating DOM directly, but by modifying data. Compared to traditional front-end development, using libraries such as jQuery to modify the DOM directly simplifies the amount of code. When interactions are complex, the logic of the code becomes very clear because the DOM becomes a mapping of the data, and all the logic is to modify the data without touching the DOM, making the code maintainable.
For example, the following uses template string syntax to render data into the DOM:
<div>{{ message }}</div>
Copy the code
var app = new Vue({
el: '#app'.data: {
message: 'Hello Vue! ',}})Copy the code
This will render Hello Vue! On the page. . The goal of this article is to figure out how templates and data are rendered into the final DOM.
What happened to New Vue
Let’s start by analyzing what happened to New Vue. Vue is actually a class, look at the source code, in the SRC/core/instance/index in 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
You can see from the code that the Vue can only be initialized with the new keyword, and then the this._init method is called. The method in the SRC/core/instance/init. Js is defined.
Vue initialization basically does a few things, merging configurations, initializing life cycles, initializing event center, initializing render, initializing Data, props, computed, Watcher, and so on.
Look at the code below, which is the end of the method.
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
At the end of initialization, if the el attribute is detected, the vm.$mount method is called to mount the VM, with the goal of rendering the template into the final DOM.
Two, Vue instance mount implementation
The VM is mounted in Vue through the $mount instance method, which is defined in multiple files because its implementation is platform – and build-dependent. The focus here is on the $mount implementation with the Compiler version.
SRC /platform/web/ entry-Runtime-with-compiler.js:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if(! options.render) {let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
template = template.innerHTML
} else {
if(process.env.NODE_ENV ! = ='production') {
warn('invalid template option:' + template, this)}return this}}else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile')}const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV ! = ='production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile end')
measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}return mount.call(this, el, hydrating)
}
Copy the code
This code first caches the $mount method on the prototype and then redefines it. The key logic in the code is that if the render method is not defined, the EL or template string is converted to the Render method. Finally, call the $mount method on the original prototype to mount it.
$mount on the original prototype method in SRC/platform/web/runtime/index defined js. Here’s what the $mount method does:
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
Copy the code
$mount takes two arguments. The first is the mounted element, which can be a string or a DOM object. If it is a string, the browser will call the Query method to convert it to a DOM object. The $mount method actually calls the mountComponent method. The methods defined in SRC/core/instance/lifecycle. The js file:
export function mountComponent(vm: Component, el: ? Element, hydrating? : boolean) :Component {
vm.$el = el
if(! vm.$options.render) { vm.$options.render = createEmptyVNodeif(process.env.NODE_ENV ! = ='production') {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0)! = =The '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
updateComponent = () = > {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} 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,
{
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
The core is to define the updateComponent method, call vm._render to create a VNode, and then call vm._update to update the DOM. You then instantiate a render Watcher, pass in the updateComponent method, and call that method.
Watcher’s two purposes here are to execute the callback function when it is initialized, and to execute the callback function when the data in the VM instance changes.
The vm._isMounted function sets vm. ismounted to true, indicating that the instance is mounted. Note that vm.$vnode represents the parent virtual Node of the Vue instance, so Null indicates that it is currently an instance of the root Vue.
Third, render
See below Vue and _render method, this method is the instance of a private method, are mainly used for the instance to render a VNode, defined in SRC/core/instance/render. The js file:
Vue.prototype._render = function () :VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(
vm._renderProxy,
vm.$createElement,
e
)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]}// return empty vnode in case the render function errored out
if(! (vnodeinstanceof VNode)) {
if(process.env.NODE_ENV ! = ='production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
Copy the code
The most critical is the render method call, as follows:
vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code
$createElement (vm.$createElement); $createElement (vm.$createElement); $createElement (vm.$createElement); While vm.$createElement is used by the user’s handwritten Render method, internally the createElement method is called.
So vm._render finally returns a VNode by executing the createElement method. Let’s look at the implementation of createElement.
Fourth, the createElement method
The createElement method is used to create a VNode. It is defined in the SRC /core/vdom/create-element.js file:
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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 with the parameters passed in. After processing these parameters, the function _createElement that actually creates VNode is called:
export function _createElement(context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
// ...
// Normalization of children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// Create a VNode
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
if( process.env.NODE_ENV ! = ='production' &&
isDef(data) &&
isDef(data.nativeOn)
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined.undefined,
context
)
} else if((! data || ! data.pre) && 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 method takes five arguments. Context represents the VNode context, which is of type Component. Tag represents a tag, which can be either a string or a Component. Data represents VNode data, which is a VNodeData type. Children represents the children of the current VNode, which is of any type and needs to be normalized as a standard VNode array; NormalizationType represents the type of child node specification, and the method varies depending on the type specification, depending on whether the Render function is compiled or handwritten.
In fact, the _createElement method has two main processes, the normalization of children and the creation of vNodes.
4.1. Standardization of children
Each VNode may have several child nodes, which should also be of the VNode type. Because the fourth argument received by _createElement is of arbitrary type, children need to be normalized to a VNode type. Depending on the normalizationType, the normalizeChildren(children) and simpleNormalizeChildren(children) methods are called. Their definitions in the SRC/core/vdom/helpers/normalize – children. Js file:
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
}
export function normalizeChildren(children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
Copy the code
SimpleNormalizationChildren method invocation scenario is render function is generated by the template compilation. Concat (array.prototype. concat) flattens the entire children Array to one level.
The normalizeChildren method can be called in two scenarios. One scenario is that the render function is written by the user, when children have only one node, and is of the basic type. In this case createTextVNode is called to create a VNode of a text node; NormalizeArrayChildren (); normalizeArrayChildren (); normalizeArrayChildren ();
function normalizeArrayChildren(children: any, nestedIndex? : string) :Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if(c ! = =' ') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (
isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)
) {
c.key = `__vlist${nestedIndex}_${i}__ `
}
res.push(c)
}
}
}
return res
}
Copy the code
NormalizeArrayChildren takes two parameters, children representing the child node to be normalized and nestedIndex representing the nestedIndex. NormalizeArrayChildren the main logic of normalizeArrayChildren is to traverse children, get a single node C, and determine the type of C. If it is an array type, recursively call normalizeArrayChildren. If it is a base type, the createTextVNode method is used to convert it to VNode. Otherwise it is already a VNode type.
After normalization of children, children becomes an Array of type VNode.
4.2 Creating vNodes
_createElement creates a VNode. Create a VNode.
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
if( process.env.NODE_ENV ! = ='production' &&
isDef(data) &&
isDef(data.nativeOn)
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined.undefined,
context
)
} else if((! data || ! data.pre) && 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
CreateComponent Creates a component VNode by createComponent, createComponent creates a component VNode by createComponent, createComponent creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. Otherwise, create a VNode with an unknown label. If the tag is a Component type, call createComponent directly to create a VNode of the Component type.
Vm. _render creates a VNode and renders it as a DOM. This is done using vm._update
Fifth, the update
Vue’s _update method is a private method of the instance. It is called at two times: first render and first data update. This article only analyzes the first rendering. The _update method renders the VNode as a real DOM. Its definition in the SRC/core/instance/liefcycle js file:
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(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)
}
// ...
}
Copy the code
The core is to call the vm.__patch__ method, which has different definitions in different platforms. The definition in the Web platform is as follows:
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
So in the browser rendering, pointing to the patch method, it is defined in SRC/platforms/web/runtime/patch. Js file:
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
This method is defined as the value returned by calling createPatchFunction. CreatePatchFunction eventually returns a patch method, which is assigned to vM. __Patch__. Let’s look at the implementation of the createPatchFunction method:
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) {
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
Back to the patch method itself, it takes four arguments. OldVnode represents the oldVnode, which may not exist or is a DOM object. Vnode represents the vnode returned by _render; Hydrating indicates whether it is server side rendering. RemoveOnly is for transition-group.
The following uses an example to analyze its execution logic.
var app = new Vue({
el: '#app'.render: function (createElement) {
return createElement('div', { attrs: { id: 'app'}},this.message)
},
data: { message: 'Hello Vue! '}})Copy the code
The patch method is called in the vm._update method:
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code
Then go back to the execution process of patch function and see several key steps:
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)
)
}
Copy the code
Since the oldVnode we passed in is actually a DOM Container, isRealElement is true. We then convert the oldVnode into a VNode object using emptyNodeAt. Then call the createElm method. CreateElm is important here, so let’s look at its implementation:
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)) {
if(process.env.NODE_ENV ! = ='production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' +
tag +
'> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
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
CreateElm creates a real DOM from a virtual node and inserts it into its parent node. Let’s look at some of its key logic. The purpose of the createComponent method is to try to create a child component, and in this case it returns false; Then check whether the VNode contains a tag. If so, verify the validity of the tag in a non-production environment to see if it is a valid tag. Then call the platform DOM operation to create a placeholder element.
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
Copy the code
Next call the createChildren method to create child elements:
createChildren(vnode, children, insertedVnodeQueue)
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
CreateChildren’s logic is simple, and it actually iterates over the child virtual nodes, recursively calling createElm, a common depth-first traversal algorithm. It is important to note that vNode. elm is passed in as a DOM node placeholder for the parent.
Then call the invokeCreateHooks method to execute all the CREATE hooks and push the vNode to insertedVnodeQueue.
if (isDef(data)) {
invokeCreateHooks(vnode, 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
Finally, insert is called to insert the DOM into the parent node. Since it is a recursive call, the child element will call INSERT first, so the insertion order of the nodes in the entire VNode tree is child before parent.
insert(parentElm, vnode.elm, refElm)
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
Insert logic is simple, call some nodeOps traverse to the child node is inserted into the parent node, auxiliary methods defined in SRC/platforms/web/runtime/node – ops. Js file:
export function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild(node: Node, child: Node) {
node.appendChild(child)
}
Copy the code
During createElm, if the vNode node does not contain a tag, it may be an annotation or plain text node that can be inserted directly into the parent element.
Back to the Patch method, the first render calls createElm, where parentElm is the parent element of oldvNode.elm. Essentially, the whole process is to recursively create an entire DOM tree and insert it into the Body.
Finally, the vNode inserts the sequential queue from the previous recursive createElm, and the associated INSERT hook function is executed.
Six, summarized
This completes the analysis of how the template and data are rendered into the final DOM from the main line, and the following figure gives a more intuitive view of the entire process from Vue initialization to final rendering.
This article has only analyzed the most basic and simplest scenarios. In practice, pages are broken down into many components, so the componentization process will be analyzed in the next article.