I’ve had a lot of interviews lately, and in almost every interview I’ve had a question about Vue source code. Here to open a series of columns, to summarize the experience of this aspect, if you feel helpful, might as well click a like support.
preface
Interview why will ask Vue source, many people are generally this question, Vue source code is almost rarely used in the ordinary work, and Vue API is often used in the work. Knowing how to use Vue’s API is not enough to meet the needs of the job. Interviews build rockets and jobs turn the screws. In fact, it is not really to ask you Vue source code, just with the help of Vue source code to assess your JavaScript foundation is solid, such as JS operation mechanism, scope, closure, prototype chain, recursion, currification and other knowledge of the degree of mastery. In order to distinguish the ability level of the interviewees. So mastering the source code of a framework in a senior front-end engineer interview is essential.
How to read the source code
Reading source code is a certain skill, if you pick up the source code directly to see, to ensure that you do not give up entry.
1, grasp the main line, temporarily ignore the branch
Rome wasn’t built in a day, and code wasn’t written in a day. Any code is developed according to the application scenario, and as the application scenario is added, the code is improved. For example, understand how templates and data in Vue are rendered into the final DOM.The first step is to write a very simple demo and study the scenario based on this setup. In the research process, the code related to the set scene is the main line, and the code irrelevant to the set scene is the branch line.
The demo code is as follows
<! DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <p>{{aa}}<span>{{bb}}</span></p> </div> </body> <script> var app = new Vue({ el: '# app' data () {return {aa: 'welcome', bb: 'Vue'}}}) < / script > < / HTML >Copy the code
2, use Chrome developer tools for breakpoint debugging
Breakpoint debugging is a skill a senior front-end engineer must master. This article uses breakpoint debugging to read the Vue source code and shows how templates and data are rendered into the final DOM in Vue.
3. Logical flow chart
Here is a logical flowchart to illustrate how templates and data are rendered into the final DOM in Vue.
Second, the new Vue ()
To use Vue, start with new Vue(), so Vue is a class constructor.
function Vue(options) { if (! (this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }Copy the code
As you can see, Vue can only be called with the new keyword. If it is not called with new, a warning is printed on the console saying that Vue is a constructor and should be called with the “new” keyword.
Execute this._init(options) to initialize the Vue, make a breakpoint here, and press F11 to enter the this._init method.
Third, enclosing _init
Vue.prototype._init = function(options) { var vm = this; if (options && options._isComponent) { //... } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } initProxy(vm); initRender(vm); initState(vm); if (vm.$options.el) { vm.$mount(vm.$options.el); }}Copy the code
$options.el = #app; $mount(vm.$options.el); Next, the mount process of Vue is analyzed.
$mount(vm.$options.el) and press F11 to enter the vm.$mount method.
1, mergeOptions
// mergeOptions if (options && options._isComponent) {} else {vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); }Copy the code
Options. _isComponent = true; false; else
Forget the internal logic of mergeOptions, just remember that after the merge, you can use vm.$options to access the parameter options passed in via new Vue(options).
2, initProxy
function initProxy(vm) { if (hasProxy) { var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; }}Copy the code
The VM Proxy to do a Proxy, through vm access to non-existent data will report an error, not to do a detailed explanation here.
Renderproxy is a Virtual DOM. Renderproxy is a Virtual DOM. Renderproxy is a Virtual DOM.
3, initRender
function initRender(vm) {
vm._vnode = null;
vm._c = function(a, b, c, d) {
return createElement(vm, a, b, c, d, false);
};
vm.$createElement = function(a, b, c, d) {
return createElement(vm, a, b, c, d, true);
};
}
Copy the code
Keep in mind that the vm._c and vm.$createElement methods are defined, which will be useful later in the vNode generation process.
4, initState ()
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) {
initProps(vm, opts.props);
}
if (opts.methods) {
initMethods(vm, opts.methods);
}
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */ );
}
if (opts.computed) {
initComputed(vm, opts.computed);
}
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
Copy the code
In this case, props, Methods, data, computed, and Watch are initialized, and in a given scenario, you just need to know how to initialize data
Opts.data exists in the specified scenario, so initData(VM) is executed.
function initData(vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (! isPlainObject(data)) { data = {}; warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; { if (methods && hasOwn(methods, key)) { warn(//..) ; } } if (props && hasOwn(props, key)) { warn(//...) ; } else if (! isReserved(key)) { proxy(vm, "_data", key); } } observe(data, true /* asRootData */ ); } function getData(data, vm) { pushTarget(); try { return data.call(vm, vm) } catch (e) { handleError(e, vm, "data()"); return {} } finally { popTarget(); } } function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }Copy the code
New Vue ({el: '# app, the data () {return {aa:' welcome ', bb: 'Vue'}}})Copy the code
In initData(vm), get the data attribute from vm.$options, which is the Vue option data. If data is a function, use getData to get the returned value and assign it to vm. Check whether the key is an object and cannot be the same as the key of props and methods.
Object.defineproperty is used to jack the data, such as vm.aa, but in reality, vm._data.aa is used to proxy the data. Accessing the VM gives you access to the value of vm._data.
This is why aa defined in the data option can be accessed using vm.aa, which is useful to remember during the generation of vNodes (Virtual DOM).
Four, vm. $mount
const mount = Vue.prototype.$mount; Vue.prototype.$mount = function(el, hydrating) { el = el && query(el); if (el === document.body || el === document.documentElement) { warn("Do not mount Vue to <html> or <body> - mount to normal elements instead."); return this } var options = this.$options; if (! options.render) { var template = options.template; if (template) { //... } else if (el) { template = getOuterHTML(el); } } if (template) { var ref = compileToFunctions(template, { outputSourceRange: "development" ! == 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render; options.staticRenderFns = staticRenderFns; } return mount.call(this, el, hydrating) }Copy the code
Prototype.$mount = Vue. Prototype.$mount This is because the $mount method is platform – and build-dependent. The $mount method is different in different environments. So cache the $mount method on the original prototype, define different $mount methods according to the scenario, and finally call the $mount method on the original prototype. This technique is called function hijacking.
1, compileToFunctions
if (template) { var ref = compileToFunctions(template, { outputSourceRange: "development" ! == 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this); var render = ref.render; options.render = render; }Copy the code
The render method is not defined in the set scene and Vue requires the Render method to generate vNodes (Virtual DOM) so fetch the Render method to vm.$options.
The method compileToFunctions uses the template, and in the scenario you don’t define template, just the mount target EL. The template will be generated through el first.
el = el && query(el);
if (el === document.body || el === document.documentElement) {
warn("Do not mount Vue to <html> or <body> - mount to normal elements instead.");
return this
}
var options = this.$options;
if (!options.render) {
var template = options.template;
if (template) {
//...
} else if (el) {
template = getOuterHTML(el);
}
}
Copy the code
The DOM object corresponding to EL is obtained by the query method and then assigned to EL. The judgment of EL is made that Vue cannot be mounted on the root node such as body and HTML.
This.$options.template does not exist, so template = getOuterHTML(el), template is the corresponding HTML content of el.
2, mountComponent
At the end of vm.$mount, return mount.call(this, el, hydrating) calls the $mount method on the original prototype. Press F11 to enter the $mount method on the original prototype.
Vue.prototype.$mount = function(el,hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
Copy the code
In the $mount method on the original prototype, the mountComponent method is finally called with the hydrating parameter related to server-side rendering, which is false in the browser environment and can be ignored.
Return mountComponent(this, EL, hydrating) Make a breakpoint here and press F11 to enter the mountComponent method.
function mountComponent(vm,el,hydrating) { vm.$el = el; var updateComponent; updateComponent = function() { vm._update(vm._render(), hydrating); }; new Watcher(vm, updateComponent, noop, { before: function before() { if (vm._isMounted && ! vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */ ); hydrating = false; if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }Copy the code
$el is the DOM object to which the Vue instance is mounted, and is assigned to vm.$el.
Instantiate a render Watcher whose second argument means a callback function. This has two functions, one is to execute the callback function at initialization time, and the other is to execute the callback function when the monitored data in the VM instance changes.
The updateComponent callback is equivalent to vm._update(vm._render(), hydrating), where vm._render() is a parameter and is executed first. Make a breakpoint at vm._update(vm._render(), hydrating) and press F11 to enter vm._render().
Five, the vm and _render
Vm. _render main function is to generate vNodes (Virtual DOM tree) and return.
Vue.prototype._render = function(){
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
//...
}
vm.$vnode = _parentVnode;
var vnode;
try{
vnode = render.call(vm._renderProxy, vm.$createElement);
}catch(e){
//...
}
return vnode
}
Copy the code
$vnode represents the parent Virtual DOM of the Vue instance. In the set scenario, _parentVnode is undefined, so vm.$vnode is undefined.
The Render method is generated from the compileToFunctions method in vm.$mount as follows.
(function anonymous() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v(_s(aa)), _c('span', [_v(_s(bb))])])])
}
})
Copy the code
The with statement specifies a default object for one or a group of statements. For example, with(this){a + b} equals this.a + this.b.
Render is called by call(vm._renderProxy, vm.$createElement), so this is vm._renderProxy, In initProxy, vm._renderProxy is the same as VM, so this is vm.
Then the Render method is equivalent to the following code
function (){
return vm._c('div', {
attrs: {
"id": "app"
}
}, [vm._c('p',
[
vm._v(vm._s(vm.aa)),
vm._c('span', [vm._v(this._s(vm.bb))])
]
)]
)
}
Copy the code
Vm. _c is defined in initRender, as well as vm.$createElement, which is not used in this scenario.
vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false) }
Copy the code
Vm. _c and vm.$createElement are both internal calls to the createElement method, with the only difference being that the last argument is true or false;
Vm. _c is used by the template’s compiled render method, and vm.$createElement is used by the user’s handwritten Render method.
const vm = new Vue({
el:'#app',
render: h => h(App)
})
Copy the code
The h in the above code is vm.$createElement.
Vnode = render. Call (vm._renderProxy, vm.$createElement), make a breakpoint here and press F11 twice to enter createElement.
var SIMPLE_NORMALIZE = 1;
var ALWAYS_NORMALIZE = 2;
function createElement(context, tag, data, children, normalizationType, alwaysNormalize) {
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
- parameter
context
: context object, that is, this; - parameter
tag
: an HTML tag name, component option object; - parameter
data
: data object of a node; - parameter
children
: Child virtual nodes (VNodes). You can also use strings to generate text virtual nodes. - parameter
normalizationType
alwaysNormalize
: Control which way to processchildren
, set the scene is not used, ignore not to see.
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
Copy the code
Check whether the second parameter data is an array or a basic type. If so, it indicates that the second parameter data should be the third parameter children. So we assign children to normalizationType, data to children, and data to undefined.
Finally call the _createElement method, make a breakpoint here, and press F11 to enter the _createElement method.
1, _createElement
function _createElement(context, tag, data, children, normalizationType){
var vnode;
if (typeof tag === 'string') {
if (config.isReservedTag(tag)) {
vnode = new VNode(config.parsePlatformTagName(tag), data,
children, undefined, undefined, context);
}
}
return vnode
}
Copy the code
The last parameter of the vm._c method is flase, so normalizationType is undefined. Children will not be handled and the core logic will be directly read.
Config. IsReservedTag (tag) indicates that the tag is a reserved HTML character. Perform new VNode (config. ParsePlatformTagName (tag), data, children, undefined, undefined, the context) generate a VNode instantiation VNode
2, new VNode ()
New VNode() instantiates a VNode to generate a VNode, which is the Virtual DOM. As a brief introduction to the Virtual DOM, DOM objects in browsers are very large. For example, if you type enter on the console, you can see that DOM objects are very large. Browser standards make DOM objects very complex. So when we manipulate DOM objects frequently, there are performance issues. Virtual DOM uses a native JS object to describe a DOM object, there is no method to operate DOM, so the cost of operating it is much less than operating DOM objects.
const div = document.createElement('div');
let str= ''
for(let key in div){
str += key + ';'
}
Copy the code
function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.context = context;
this.key = data && data.key;
this.parent = undefined;
}
Copy the code
Just focus on these attributes in the VNode constructor and ignore the rest in the scenario
tag
: the tag namedata
Data:children
Son: vnodetext
: Indicates the text of the current nodeelm
: The corresponding real DOM objectparent
Father: vnode
In the set scene, the DOM object generated, as shown in the figure below, is a tree structure, so the corresponding Virtual DOM should also be a tree structure, so it is related to the third parameter of the VNode function children, Children are passed in as the fourth argument to the _createElement function, which is actually called in the generated render method and goes back to the generated render method (which does the same).
function (){
return vm._c('div', {
attrs: {
"id": "app"
}
}, [vm._c('p',
[
vm._v(vm._s(vm.aa)),
vm._c('span', [vm._v(this._s(vm.bb))])
]
)]
)
}
Copy the code
When executing a function, if the argument is a function, execute it first.
-
Implement vm. _c (‘ div ‘.. , meet parameters [vm. _c (‘ p ‘, [vm. _v (vm) _s (vm) aa)), vm. _c (” span “, [vm. _v (vm) _s (vm. Bb))])])];
-
Implement vm. _c (‘ p ‘, [vm. _v (vm) _s (vm) aa)), vm. _c (” span “, [vm. _v (vm) _s (vm. Bb))]]]), meet parameters [vm. _v (vm) _s (vm) aa)), vm. _c (‘ span, [vm. _v (vm) _s (vm. Bb))])];
-
Implement vm. _v (vm) _s (vm) aa)) and vm. _c (” span “, [vm. _v (vm) _s (vm. Bb))]), met parameter vm. _s (vm) aa) and (vm) _v (vm) _s (vm. Bb))]
-
Implement vm. _s (vm) aa) and vm _v (vm) _s (vm. Bb)), met parameter vm. _s (vm) bb)
-
Implement vm. _s (vm. Bb)
According to the above analysis, vm._s(vm.aa) and vm._s(vm.bb) are executed first, followed by vm._v(vm._s(vm.bb)).
Define vm._v and vm._s in the installRenderHelpers function. When ue. Js is loaded, renderMixin(Vue) is executed, where installRenderHelpers(Vue. Prototype) is executed. So vm._v corresponds to createTextVNode and vm._s corresponds to toString.
function renderMixin (Vue) {
installRenderHelpers(Vue.prototype)
}
function installRenderHelpers(target) {
target._s = toString;
target._v = createTextVNode;
}
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
function toString (val) {
return val == null ? '' :
Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2) : String(val)
}
Copy the code
-
The createTextVNode method is used to create a Virtual DOM of a text node. Note that its argument is text (the text of the current node), the fourth argument passed to the new VNode.
-
The toString method is used to convert any value to a string.
Then implement vm. _v (vm) _s (vm. Bb)) get a vnode, remember to vnode1, to implement vm. _c (” span “, [vm. _v (vm) _s (vm. Bb))]), quite implement vm. _c (” span “, [vnode]), Another vnode is obtained, denoted as vnode2, where the children property value of vnode1 is [vnode2].
And so on, layer by layer. When performing thevm._c('div'..
The result is a Virtual DOM tree, as shown.
In vm._render, the template and data are generated into a Virtual DOM tree, and in vm._update, the Virtual DOM tree is rendered into a real DOM tree.
Six, vm. _update
Vue.prototype._update = function(vnode, hydrating) {
var vm = this;
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */ );
} else {
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
Copy the code
PrevVnode = vm._vnode, vm._vnode is the Virtual DOM generated by the current Vue instance. Vm. _vnode is the first render in the set scene. PrevVnode = undefined, then vm._vnode = vnode to assign the Virtual DOM generated by the current Vue instance to vm._vnode.
PrevVnode is undefined, so vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) to render DOM for the first time.
Vue.prototype.__patch__ = inBrowser ? patch : noop;
Copy the code
In browser, vue.prototype. __patch__ is patch.
1, the patch
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
Copy the code
Patch is generated by createPatchFunction. NodeOps is a method that encapsulates a series of DOM operations, and modules is a hook function that defines modules. We won’t go into details here, but look at the implementation of createPatchFunction.
export function createPatchFunction(backend) { const {modules,nodeOps} = backend return function patch(oldVnode, vnode, hydrating, removeOnly) { //... }}Copy the code
Before we do that, consider why createPatchFunction is used to generate a patch method.
Patch is environment dependent. In the Web and Weex environments, there are nodeOps and Modules. However, the main logic of patches in different environments is the same, and the differentiation part only needs to be differentiated by parameters. The skill of function Currification is used here, and the differentiation parameter is solidified in advance by createPatchFunction, without passing the corresponding nodeOps and modules every time patch is called. This programming skill is well worth learning.
$el = vm.__patch__(vm.$el, vnode, hydrating, false).$el = vm.__patch__(vm.$el, vnode, hydrating, false)
function patch(oldVnode, vnode, hydrating, removeOnly) { const insertedVnodeQueue = [] if (isUndef(oldVnode)) {} else { const isRealElement = isDef(oldVnode.nodeType) if (! isRealElement && sameVnode(oldVnode, vnode)) {} else { if (isRealElement) { oldVnode = emptyNodeAt(oldVnode) } const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm)) if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) {} } } return vnode.elm }Copy the code
- parameter
oldVnode
: The last Virtual DOM, the value set in the scene isvm.$el
Is a DOM object; - parameter
vnode
: Virtual DOM this time; - parameter
hydrating
: in non-server rendering casesfalse
, can be ignored; - parameter
removeOnly
: it is intransition-group
Used in the scene, not in the setting scene, forfalse
, can be ignored.
If oldVnode is not a Virtual DOM but a DOM object, convert oldVnode to a Virtual DOM with emptyNodeAt and the converted DOM object on elm assignment, so oldElm equals vm.$el, Nodeops. parentNode(oldElm) is used to obtain the parent DOM node of oldElm, which is body.
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
//nodeOps.parentNode
function parentNode (node) {
return node.parentNode
}
Copy the code
CreateElm (vnode, insertedVnodeQueue, parentElm, nodeops.Nextsibling (oldElm)) Nodeops. nextSibling(oldElm) fetch the nextSibling of oldElm.
2, createElm
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
vnode.isRootInsert = !nested;
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode);
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
} else if (isTrue(vnode.isComment)) {
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
Copy the code
- parameter
vnode
: Virtual DOM; - parameter
insertedVnodeQueue
: hook function queue; - parameter
parentElm
Parameters:vnode
The parent DOM object corresponding to the real DOM object; - parameter
refElm
: placeholder node object, for example, parametervnode
Corresponding to the next sibling of the DOM object; - parameter
nested
Judge:vnode
Is the Virtual DOM of the root instance; - parameter
ownerArray
: an array of its children - parameter
index
: subscript of the child node array
Parameters ownerArray and index are used to solve the error caused by elm overwriting vNode, which was previously rendered as a new Virtual DOM. The scene set is the first rendering, which can be ignored.
vnode.tag
If yes, run the commandnodeOps.createElement(tag, vnode)
Generates a real DOM node and assigns a value tovnode.elm
.nodeOps.createElement
The corresponding iscreateElement
Methods.
function createElement (tagName, vnode) { var elm = document.createElement(tagName); if (tagName ! == 'select') { return elm } if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple ! == undefined) { elm.setAttribute('multiple', 'multiple'); } return elm }Copy the code
vnode.tag
If not, it may be a comment or plain text node, executenodeOps.createTextNode(vnode.text)
Generates a comment or plain text node and assigns a value tovnode.elm
.nodeOps.createTextNode
The corresponding iscreateTextNode
Methods.
function createTextNode (text) {
return document.createTextNode(text)
}
Copy the code
CreateChildren (vnode, Children, insertedVnodeQueue) is the child Virtual DOM of a Vnode. Press F11 to enter the createChildren method
3, createChildren
function createChildren(vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { checkDuplicateKeys(children); for (var 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. It actually traverses the vNode child Virtual DOM, recursively calling createElm, a common depth-first traversal algorithm. In the traversal process, children[I] (Virtual DOM) of vnode.elm will be passed in corresponding to the parent node of the real DOM.
When children are not an array. Call nodeops.createTextNode to generate a plain text node and insert nodeops.appendChild into vnode.elm.
4, the insert
Execute insert(parentElm, vnode.elm, refElm) to insert the generated DOM (vnode.elm) into the corresponding parent node (parentElm). Since it is recursive, the child Virtual DOM will call insert first. Therefore, the insertion order of the entire Virtual DOM tree after generating the real DOM is child first and then parent.
Make a breakpoint at insert(parentElm, vnode.elm, refElm) and press F11 to enter the insert method.
function insert(parent, elm, ref$$1) { if (isDef(parent)) { if (isDef(ref$$1)) { if (nodeOps.parentNode(ref$$1) === parent) { nodeOps.insertBefore(parent, elm, ref$$1); } } else { nodeOps.appendChild(parent, elm); }}}Copy the code
- parameter
parent
: The parent node of the node to be inserted - parameter
elm
: Node to be inserted - parameter
ref$$1
: reference node, will be inserted before the reference node
Nodeops. insertBefore corresponds to the insertBefore method, nodeops. appendChild corresponds to the appendChild method,
function insertBefore (parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
function appendChild (node, child) {
node.appendChild(child);
}
Copy the code
The insertBefore and appendChild methods call the native DOM API for DOM manipulation.
5. Back to createElm
When the createChildren call is complete and the vNode child Virtual DOM is traversed, go back to the original createElm method and execute insert(parentElm, vnode.elm, refElm). The value of the parentElm parameter is executed in the patch method
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
Copy the code
OldElm =
{{aa}}{{bb}}
Vnode. elm generates a real DOM for the Virtual DOM tree.
The entire process is essentially a recursive call to createElm that creates a real DOM tree and inserts it into the body.
When you look at this, you may realize that the Vue creates the DOM dynamically like this.
In addition, the invokeCreateHooks method executes a number of hook functions, such as adding the data attrs: {id: “app”} attribute to the div tag, which is not covered here.
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
}
Copy the code
{{aa}}{{bb}}
is removed from the parent node of the original template by last calling removeVnodes in the patch method because parentElm exists. This also explains why there is a restriction on the mount target of Vue instances, which cannot be mounted to body or HTML objects.
Back in the vm._updata method, update the vm.$el method because the patch method returns the new DOM object to which the current Vue instance is mounted.
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
Copy the code
6. Return to mountComponent
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
Copy the code
Vue.$vnode specifies the parent Virtual DOM of the Vue instance. If vnode is null, the vm is the root Vue instance.
Seven,
The key process for rendering templates and data into the final DOM can be summed up in one sentence:
Virtual DOM tree is generated from child to parent in vm._render, real DOM is generated from parent to child in vm._update, real DOM is generated from child to parent and inserted into the corresponding parent node to generate a real DOM tree, and finally inserted into the body.