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
  • parametercontext: context object, that is, this;
  • parametertag: an HTML tag name, component option object;
  • parameterdata: data object of a node;
  • parameterchildren: Child virtual nodes (VNodes). You can also use strings to generate text virtual nodes.
  • parameternormalizationType 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 name
  • dataData:
  • childrenSon: vnode
  • text: Indicates the text of the current node
  • elm: The corresponding real DOM object
  • parentFather: 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
  • parameteroldVnode: The last Virtual DOM, the value set in the scene isvm.$elIs a DOM object;
  • parametervnode: Virtual DOM this time;
  • parameterhydrating: in non-server rendering casesfalse, can be ignored;
  • parameterremoveOnly: it is intransition-groupUsed 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
  • parametervnode: Virtual DOM;
  • parameterinsertedVnodeQueue: hook function queue;
  • parameterparentElmParameters:vnodeThe parent DOM object corresponding to the real DOM object;
  • parameterrefElm: placeholder node object, for example, parametervnodeCorresponding to the next sibling of the DOM object;
  • parameternestedJudge:vnodeIs the Virtual DOM of the root instance;
  • parameterownerArray: an array of its children
  • parameterindex: 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.tagIf yes, run the commandnodeOps.createElement(tag, vnode)Generates a real DOM node and assigns a value tovnode.elm.nodeOps.createElementThe corresponding iscreateElementMethods.
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.tagIf 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.createTextNodeThe corresponding iscreateTextNodeMethods.
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
  • parameterparent: The parent node of the node to be inserted
  • parameterelm: Node to be inserted
  • parameterref$$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.