When we mount through an instance of new Vue(), we replace the corresponding mounted DOM node. Now we through the source point of view analysis of its implementation behind the main process.
Vue constructor
Let’s look at a simple example of Vue:
<div id="app">{{ msg }}</div>
<script>
new Vue({
el: '#app',
data: {
msg: 'hello vue'}});</script>
Copy the code
Now let’s look at the definition of the Vue constructor. It defined in 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)
}
// Define various methods on vue. prototype
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
Copy the code
See that Vue is a constructor that receives an Option configuration object. The following mixin methods define methods on top of Vue prototypes. For example, _init() is defined in initMixin when new Vue() is called. Let’s look at the implementation of _init().
vm._init()
Vm. _init () method defined in SRC/core/instance/init. Js, we delete the performance monitoring of relevant code:
let uid = 0
Vue.prototype._init = function (options? : Object) {
const vm: Component = this
vm._uid = uid++
/ /... Performance monitoring correlation
// The tag of the VM instance
vm._isVue = true
if (options && options._isComponent) {
// Configuration merge of component instances
initInternalComponent(vm, options)
} else {
/ / general configuration of the Vue instance merger, mainly put some component of the global directive, merge the filter to the vm
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
initLifecycle(vm) // Maintain vm. parent and vm. children
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm) // Handle status monitoring
initProvide(vm)
callHook(vm, 'created')
// Use new Vue to mount components without going there
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Copy the code
The initInternalComponent() method handles the configuration of the component instance. The mergeOptions method is used in this example. . It is mainly the Vue option on some of the global component, directive, filter merge to vm, merge strategy within the code we again after detailed analysis.
After that, different module initializations of various instantiations, such as the initState() method, handle state-related code, such as response handling of data data, etc. The callHook() method executes the lifecycle hook, and initState() is called after the beforeCreate hook, which is why we can only get the VM state from the CREATE hook.
Finally, if the EL is provided in the new Vue() configuration object, we call the vm.$mount() method to mount it. Let’s examine the code for the $mount() method.
vm.$mount()
The way instances are mounted is platform-specific, and the compile and runtime entry is different. The entry point for the compiled version is to handle the configuration object Template on a run-time basis, turning the template string into the Render () method. So we start with the compile version of the mount entry, which is defined in SRC /platforms/web/entry-runtime-with-compiler.js:
// Mount the runtime version of the cache
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
// Return the DOM object of el
el = el && query(el)
// Cannot be mounted in body or document
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
// If options does not have a render method, get render according to template or el
if(! options.render) {let template = options.template
if (template) {
// Process template as an HTML string
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template)
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) {
// use the outhTML of the DOM corresponding to el as template
template = getOuterHTML(el)
}
if (template) {
// Compile template to get render
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
}
}
// Call the Runtime mount method
return mount.call(this, el, hydrating)
}
Copy the code
We start by running the version’s mount function with a variable cache, and then define the compiled version’s $mount() method. Method handles the template to process the configuration object and, if provided, the various configurations and finally the HTML string. This example doesn’t have a template, so get the OuterHTML of the DOM corresponding to el as the template.
Once we get the template we call compileToFunctions() to compile the template into the render function. The template is handled in vue using the virtual DOM, and the render() method is used to obtain the virtual DOM node of the corresponding template. Finally, call the mount method of the run version.
Running version of the mount method defined in SRC/platforms/web/runtime/index in js:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
Copy the code
The method is simple: turn the EL into a DOM object and call the mountComponent() method. The methods defined in SRC/core/instance/lifecycle. In js:
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') {
/ /.. An error is reported if template exists}}Call the beforeMount hook function
callHook(vm, 'beforeMount')
let updateComponent
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
/ /.. Performance monitoring correlation
} else {
updateComponent = (a)= > {
vm._update(vm._render(), hydrating)
}
}
// Create a new render watcher that calls the get() method in the constructor
// updateComponent will be executed
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true)
hydrating = false
$vnode represents the parent vnode, which is called only by the root instance
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')}return vm
}
Copy the code
The render method initially determines if the Render method is present, returns a method to create an empty node if not, and checks in the development environment to see if the version is running with the template configuration. We then define an updateComponent function as an argument to create a new Watcher instance, regardless of the implementation of Watcher, as long as we know that the updateComponent() method will be executed during Watcher instantiation. After executing this method, check whether the VM is the root instance. If so, call mounted.
vm._render()
After the updateComponent() method is executed, the vm._render() method first returns the virtual node corresponding to the instance. A virtual node is a simple REPRESENTATION of a DOM node using an ordinary JS object. Vm. And _render () method defined in SRC/core/instance/render in js:
Vue.prototype._render = function () :VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// Save the parent virtual node
vm.$vnode = _parentVnode
let vnode
try {
currentRenderingInstance = vm
// Call the render function on the VM
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
/ /... Error correlation handling
} finally {
currentRenderingInstance = null
}
// ...
// set parent
vnode.parent = _parentVnode
return vnode
}
Copy the code
This method calls the render() method on options with vm.$createElement as an argument. Vm.$createElement is defined as:
vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)
Copy the code
Obviously, this method is a wrapper around the createElement() method, which creates a virtual node. So we can use this argument ourselves when we write the render() function, for example:
new Vue({
el: '#app'.data: {
msg: 'hello vue'
},
render(h) {
return h('div', {}, this.msg)
}
});
Copy the code
The h parameter is vm.$createElement. We won’t use this parameter in our example, because we didn’t write the render function by hand, but used vue compiled render. This time we call vm._c:
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
Copy the code
The difference between the two is the last parameter. Let’s look at the definition of the createElement() function. Define in SRC /core/vdom/create-element.js:
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
// Data is not transmitted
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
// Format children
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
Copy the code
This method first deals with cases where the data parameter is not a property configuration object. Then call the _createElement() method, which is defined in the same file. This method handles the children node first, and in the case of a hand-written render function, converts children to an array of vNode objects:
if (Array.isArray(children) &&
typeof children[0= = ='function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
Copy the code
The instance is a virtual node created by calling vm._c, so the simpleNormalizeChildren() method is used, which flattens the array of children once, but only one layer.
Then, based on the tag judgment, call different methods to return the VNode. In this example, the tag is a div, which is a built-in tag for the platform.
if (config.isReservedTag(tag)) {
// Platform reserved tags
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
)
}
Copy the code
New VNode() returns a VNode object directly. The createComponent() method creates an implementation of the component node vNode, which we’ll examine later. At this point. Vnode = vnode; vnode = vnode; vnode = vnode; vnode = vnode;
vm._update()
Call vm._update() with the virtual vNode returned after the vm._render() method. It defined in SRC/core/instance/lifecycle. In js:
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
if(! prevVnode) {// Mount it for the first time
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
Copy the code
This method updates the DOM by calling the vm.__patch__() method to compare the old and new virtual nodes and find the difference caused by the state change. Return the updated DOM node and assign it to vm.$el. For the first mount:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code
The definition of vm.__patch__() is:
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
The definition of patch method is:
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
The Patch () method is returned by calling createPatchFunction(), where nodeOps is the wrapper around the DOM manipulation method, and Modules is the hook function that is called to create the VNode comparison procedure. Since VUE can be implemented cross-platform, the most important point of cross-platform is that the operation and parsing methods of Vnode are different, so a factory function is used to return the corresponding patch method.
Let’s look at the createPatchFunction() method. The createPatchFunction() method defines the utility functions used in the patch process. The code is quite long.
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
export function createPatchFunction(backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// Collect hook functions
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]])
}
}
}
/ /... Encapsulation of patch process function
/ / returns the 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) // Whether it is a DOM node, the first mount is true
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
// Diff on the same node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
} else {
if (isRealElement) {
/ / SSR
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.')}}// Create a simple vnode
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// Create a DOM with vnode and insert it
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// destroy old node
if (isDef(parentElm)) {
// Delete the old node
removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
Copy the code
Since our oldVnode was first mounted as vm.$el, the DOM object corresponding to el in the configuration, the isRealElement variable is true. So the following flow code is used:
if (isRealElement) {
/ /... SSR related
// Create a simple vnode
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// create new node
// Create a DOM with vnode and insert it
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
// destroy old node
if (isDef(parentElm)) {
// Delete the old node
removeVnodes([oldVnode], 0.0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
Copy the code
The emptyNodeAt() method is used to convert the actual DOM into a virtual node.
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
Copy the code
This method stores the actual DOM in vnode.elm.
const parentElm = nodeOps.parentNode(oldElm);
Copy the code
Get the parent of the actual DOM, which in our case is the body node. Call createElm() to create a new DOM node for vNode and insert it into parentElm.
// Create a DOM with vnode and insert it
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if(isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode); } vnode.isRootInsert = ! nested;// Process of creating a vnode
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 ); }}// Create a label node
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
if (__WEEX__) {
/ /... Weex processing
} else {
// Recursively create the children dom node of vnode and insert it into vnode.elm
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// Insert the node created by vNode into the real DOM
insert(parentElm, vnode.elm, refElm);
}
if(process.env.NODE_ENV ! = ='production'&& data && data.pre) { creatingElmInVPre--; }}else if (isTrue(vnode.isComment)) {
// Create a comment node
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
// Create a text nodevnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); }}Copy the code
The createComponent() method is handled by the component vNode, where we return false to continue the logic. Nodeops.createelement () generates an empty DOM node and then calls createChildren() to insert the child node into the DOM created by the current VNode:
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) {
// recursive call
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
Call invokeCreateHooks() after insertion to execute the Create hook defined on moudles and vNode. This process mainly deals with the various properties defined by data on vNode, such as class, event, etc. They are in the implementation of the SRC/platforms/web/runtime/modules. Finally, we call the insert() method to insert the generated new DOM into the parent node, in this case the body. Let’s look at the definition of the insert() method:
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
We’ll look at two inserted in nodeOps method, they are all defined in SRC/platforms/web/runtime/node – ops. In js:
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
Obviously, this is all native DOM manipulation. So, after executing, view the DOM as
After inserting the DOM, remove the old nodes by calling removeVnodes() :
// Delete the old node
removeVnodes([oldVnode], 0.0)
Copy the code
Finally, the invokeInsertHook() method is called to execute the various hook functions after DOM insertion. At this point, the DOM corresponding to our new Vue() has been most successful in replacing and calling the original mounted node.
conclusion
New Vue() calls the _init() method of the instance for initialization, and then calls the $mount() method for mounting. Mount a new rendered Watcher and immediately execute the Getter for the Watcher. This is the updateComponent() function, which generates the virtual node with vm._render() and then calls vm._update() to patch the node. The patch process deals with oldVnode as a real DOM element. The process is probably to generate the DOM with the new VNode and then tune the original DOM instead.
It can be summarized as follows:
>>> Original address