🚩Vue源码——组件如何渲染成最终的DOM

最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。

前言

Vue有两个核心思想,一个是数据驱动,简单来说就是通过模板和数据渲染成最终的 DOM ,具体是如何实现在上一篇🚩Vue源码——模板和数据如何渲染成最终的DOM中详细地介绍过了。

另外一个是组件化,谓组件化,就是把一个页面拆分成多个组件,这些组件是独立的,可复用的,可嵌套的,等这些组件开发完成后,像搭积木一样拼装成一个页面。

本文会在上一篇的基础上来详细介绍在 Vue 中组件如何渲染成最终的 DOM ,其过程与通过模板和数据渲染成最终的 DOM 有何不同。

先创建一个简单的 demo ,基于这个设定的场景来研究。

<!DOCTYPE html>
<html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    </head>
    <body>
        <div id="app"></div>
    </body>
    <script>
        const aa ={
            template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
            data(){
                return{
                    aa:'欢迎',
                    bb:'Vue'
                }
            }
        }
        var app = new Vue({
            el: '#app',
            render: h =>h(aa)
        })
    </script>
</html>

回顾上一篇模板和数据渲染成最终的 DOM 的逻辑流程图

跟组件渲染成最终的 DOM 的逻辑流程图对比。

new Vue()new Watcher()过程都是一样的。因为在上一篇 demo 中 render 方法是编译生成,在本文 demo 中 render 方法是用户手写的render: h =>h(aa),所以从vm.render开始不一样,那么从vm.render开始介绍组件如何渲染成最终的 DOM 。

一、vm._render

vm._update(vm._render(), hydrating),在此处打个断点,按F11进入vm._render()方法中**。

Vue.prototype._render = function() {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentVnode = ref._parentVnode;
    vm.$vnode = _parentVnode;
    var vnode;
    try {
        currentRenderingInstance = vm;
        vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
        //...
    } finally {
        currentRenderingInstance = null;
    }
    return vnode;
}

此时的vnode = render.call(vm._renderProxy, vm.$createElement)中的render是用户手写的render方法render: h =>h(aa),其中hvm.$createElement

vm.$createElement是在 Vue 初始化中通过initRender(vm)定义的。

function initRender(vm) {
    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);
    };
}

可以看到调用vm.$createElement实际上是调用createElement(vm, a, b, c, d, true)

vnode = render.call(vm._renderProxy, vm.$createElement) 在此处打个断点,按三次F11进入createElement方法中

1、_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)
}

要注意参数alwaysNormalizetrue,故normalizationType有值为2。

最后调用_createElement,此处打个断点,按F11进入_createElement方法中

function _createElement(context, tag, data, children, normalizationType) {
    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children);
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children);
    }
    var vnode;
    if (typeof tag === 'string') {
        //...
    } else {
        vnode = createComponent(tag, data, context, children);
    }
    return vnode
}

从设定的场景中,从render: h =>h(aa)可以得知,参数data、参数children为 undefined ,故即是参数normalizationType为 2 等于ALWAYS_NORMALIZE,也可以忽略children = normalizeChildren(children)的逻辑过程。

参数tag为组件 aa 的选项对象。

const aa ={
    template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
    data(){
        return{
            aa:'欢迎',
            bb:'Vue'
        }
    }
}

故直接走 else 中的逻辑,调用createComponent生成组件类型的vnodevnode = createComponent(tag, data, context, children)在此处打个断点,按F11进入createComponent方法中

2、createComponent

function createComponent(Ctor, data, context, children, tag) {
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor);
    }
    data = data || {};
    installComponentHooks(data);
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
        ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
        data, undefined, undefined, undefined, context, {
            Ctor: Ctor,
            tag: tag,
            children: children
        }
    );
    return vnode
}

以上代码有三个关键步骤,构造子类构造函数,安装组件钩子函数和实例化 VNode 。

1、构造子类构造函数baseCtor.extend

var baseCtor = context.$options._base;
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
}

_baseinitGlobalAPI函数中定义。

function initGlobalAPI(Vue) {
    Vue.options._base = Vue;
}

又在this._init(options)中,把options合并到$options上。

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
);

所以baseCtor实际上就是 Vue 构造函数,再来看一下Vue.extend函数的定义。

var ASSET_TYPES = ['component','directive','filter'];
Vue.extend = function(extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
        return cachedCtors[SuperId]
    }

    var name = extendOptions.name || Super.options.name;
    if (name) {
        validateComponentName(name);
    }

    var Sub = function VueComponent(options) {
        this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;

    Sub.cid = cid++;
    Sub.options = mergeOptions(
        Super.options,
        extendOptions
    );
    Sub['super'] = Super;

    if (Sub.options.props) {
        initProps$1(Sub);
    }
    if (Sub.options.computed) {
        initComputed$1(Sub);
    }

    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;

    ASSET_TYPES.forEach(function(type) {
        Sub[type] = Super[type];
    });

    if (name) {
        Sub.options.components[name] = Sub;
    }

    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);
    cachedCtors[SuperId] = Sub;
    return Sub
}

Vue.extend的作用就是创建一个 Vue 的子类 Sub。

var Sub = function VueComponent(options) {
    this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;

这里采用原型链继承和借用构造函数继承的组合继承方式,创建一个继承于 Vue 的子类 Sub 并返回。

借用构造函数继承好理解,在子类 Sub 的构造函数VueComponent(options)中调用父类 Vue 的初始化方法this._init(options)。这样实例化子类 Sub 时就会执行this._init(options),就再次走到 Vue 的初始化过程。

原型链继承为什么不采用Sub.prototype = new Vue(),因为这样做,有个缺点创建子类 Sub 实例时,要调用两次父类 Vue。

采用Sub.prototype = Object.create(Super.prototype)来实现原型链继承,不会再去调用父类 Vue。然后再把子类 Sub 的构造器constructor重新指向Sub

var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
}
Sub.cid = cid++;
cachedCtors[SuperId] = Sub;
return Sub

在创建过程中,对子类 Sub 做了缓存,避免多次执行Vue.extend时对同一个组件重复创建子类 Sub。

2、安装组件钩子函数installComponentHooks

data = data || {};
installComponentHooks(data);

installComponentHooks此处打个断点,按F11进入installComponentHooks方法中

var componentVNodeHooks = {
    init: function init(vnode, hydrating) {
        //...
    },
    prepatch: function prepatch(oldVnode, vnode) {
        //...
    },
    insert: function insert(vnode) {
        //...
    },
    destroy: function destroy(vnode) {
        //...
    }
};
var hooksToMerge = Object.keys(componentVNodeHooks);

function mergeHook(f1, f2) {
    var merged = function(a, b) {
        f1(a, b);
        f2(a, b);
    };
    merged._merged = true;
    return merged
}

function installComponentHooks(data) {
    var hooks = data.hook || (data.hook = {});
    for (var i = 0; i < hooksToMerge.length; i++) {
        var key = hooksToMerge[i];
        var existing = hooks[key];
        var toMerge = componentVNodeHooks[key];
        if (existing !== toMerge && !(existing && existing._merged)) {
            hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
        }
    }
}

整个installComponentHooks的过程就是把componentVNodeHooks中的钩子函数合并到data.hook中,在合并过程中,如果某个钩子函数已经存在data.hook中,通过mergeHook方法做合并,在mergeHook方法中,返回一个依次执行这两个钩子函数的函数,即完成合并。

此小节要记住data.hook存储了组件的钩子函数

3、new VNode

var name = Ctor.options.name || tag;
var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context, {
        Ctor: Ctor,
        tag: tag,
        children: children
    }
);
return vnode

先来看一下 VNode类的构造函数,忽略掉跟设定的场景无关的代码。

var VNode = 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.componentOptions = componentOptions;
    this.componentInstance = undefined;
}

需要注意组件的vnode和普通元素节点的vnode不同,组件的vnode是没有children的。

执行完vm._render()生成vnode,回到vm._update中,分析vnode如何生成真实 DOM 。

二、vm._update

Vue.prototype._update = function(vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    if (!prevVnode) {
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    } else {
        vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
}

执行var prevVnode = vm._vnodevm._vnode是当前 Vue 实例生成的 Virtual DOM ,在设定的场景中是首次渲染,此时vm._vnode为 undefined ,故prevVnode为 undefined ,再执行vm._vnode = vnode,把当前 Vue 实例生成的 Virtual DOM 赋值给vm._vnode

因为prevVnode为 undefined ,故执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false).

上一篇文章中,介绍了vm.__patch__是如何定义的。

执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false),最终是调用patch方法,在此处打个断点,按F11进入patch方法中

1、patch

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
}
  • 参数oldVnode:上一次的 Virtual DOM ,设定的场景中的值为vm.$el,是<div id="app"></div> DOM 对象;
  • 参数vnode:这一次的 Virtual DOM ;
  • 参数hydrating:在非服务端渲染情况下为 false,可以忽略;
  • 参数removeOnly: 是在transition-group场景下用,设定场景中没有,为false,可以忽略。

如果oldVnode不是 Virtual DOM 而是 DOM 对象,要把oldVnodeemptyNodeAt转成一个 Virtual DOM,并在其属性elm赋值上被转换的 DOM 对象,所以oldElm等同vm.$el,在用nodeOps.parentNode(oldElm)获取oldElm的父级 DOM 节点,此时为 body。

function emptyNodeAt (elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
//nodeOps.parentNode
function parentNode (node) {
    return node.parentNode
}

执行createElm(vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm))生成真实 DOM ,在此处打个断点,按F11进入createElm方法中,nodeOps.nextSibling(oldElm)oldElm的下一个兄弟节点。

2、createElm

function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
}
  • 参数vnode: Virtual DOM;
  • 参数insertedVnodeQueue:钩子函数队列;
  • 参数parentElm: 参数vnode对应 DOM 对象的父节点 DOM 对象;
  • 参数refElm: 占位节点对象,例如,参数vnode对应 DOM 对象的下个兄弟节点;

在设定场景中,是要把组件渲染成 DOM ,会调用createComponent方法,在此处打个断点,按F11进入createComponent方法中

3、createComponent

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false);
        }
        if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue);
            insert(parentElm, vnode.elm, refElm);
            if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
            }
            return true
        }
    }
}

执行完var i = vnode.data;isDef(i = i.hook) && isDef(i = i.init),此时ivnode.data.hook中的init方法。init方法在componentVNodeHooks中定义,通过installComponentHooks方法将其合并到data.hook中。

i(vnode, false)在此处打个断点,按F11进入init方法中

此外还有注意到insert(parentElm, vnode.elm, refElm)这句代码,先提一下这句代码的作用是把组件内容生成的 DOM 插入父节点中,后面会详细介绍。

4、componentVNodeHooks.init

var componentVNodeHooks = {
    init: function init(vnode, hydrating) {
        if (vnode.componentInstance 
            &&!vnode.componentInstance._isDestroyed 
            && vnode.data.keepAlive) {
            //...
        } else {
            var child = vnode.componentInstance  = 
                createComponentInstanceForVnode( vnode, activeInstance);
            child.$mount(hydrating ? vnode.elm : undefined, hydrating);
        }
    },
}

vnode.componentInstance的含义是组件实例,此时组件实例还没创建,故vnode.componentInstance为 undefined,走 else 部分逻辑。通过createComponentInstanceForVnode方法创建一个 Vue 实例 child,调用$mount方法挂载组件。

createComponentInstanceForVnode( vnode, activeInstance)在此处打个断点,按F11进入createComponentInstanceForVnode方法

5、createComponentInstanceForVnode

function createComponentInstanceForVnode(vnode, parent) {
    var options = {
        _isComponent: true,
        _parentVnode: vnode,
        parent: parent
    };
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
        options.render = inlineTemplate.render;
        options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
}
  • 参数vnode: 要渲染的组件生成的 Virtual DOM。
  • 参数parent: 要渲染的组件的父 Vue 实例,也就是上下文环境。

还记得在createComponent方法中,通过Ctor = baseCtor.extend(Ctor)创建了一个 Vue 的子类(组件)构造函数赋值给Ctor,然后在实例化 Vnode 中,通过 Vnode 构造函数的参数传递给vnode的属性componentOptions。所以vnode.componentOptions.Ctor就是要渲染组件的构造函数,new一下来生成组件实例。

new vnode.componentOptions.Ctor(options)在此处打个断点,按F11进入。会发现走到Vue.extend方法中的

var Sub = function VueComponent (options) {
    this._init(options);
}

执行this._init(options),进行组件构造函数的初始化,又回到 Vue 构造函数的初始化,按F11进入this._init中,开始介绍组件内容的渲染过程

6、组件内容的渲染过程

1、this._init

Vue.prototype._init = function(options) {
    var vm = this;

    if (options && options._isComponent) {
        initInternalComponent(vm, options);
    } else {
        //...
    }

    if (vm.$options.el) {
        vm.$mount(vm.$options.el);
    }
}

因为在createComponentInstanceForVnode方法中设置了options = {_isComponent : true}, 故options._isComponenttrue,执行initInternalComponent(vm, options)来合并options在此次打个断点,进入initInternalComponent方法中

2、initInternalComponent

function initInternalComponent(vm, options) {
    var opts = vm.$options = Object.create(vm.constructor.options);
    var parentVnode = options._parentVnode;
    opts.parent = options.parent;
    opts._parentVnode = parentVnode;

    var vnodeComponentOptions = parentVnode.componentOptions;
    opts.propsData = vnodeComponentOptions.propsData;
    opts._parentListeners = vnodeComponentOptions.listeners;
    opts._renderChildren = vnodeComponentOptions.children;
    opts._componentTag = vnodeComponentOptions.tag;

    if (options.render) {
        opts.render = options.render;
        opts.staticRenderFns = options.staticRenderFns;
    }
}

还记得用Vue.extend方法创建组件的构造函数时,有执行以下一段代码

Vue.extend = function(extendOptions){
    Sub.options = mergeOptions(
        Super.options,
        extendOptions
    );
}

createComponent中执行Ctor = baseCtor.extend(Ctor)中调用Vue.extend,参数Ctor为 demo 中 aa 组件的选项对象,即extendOptions的值为

{
    template:'<div><p>{{aa}}<span>{{bb}}</span></p></div>',
    data(){
        return{
            aa:'欢迎',
            bb:'Vue'
        }
    }
}

经过mergeOptions方法合并,可以通过构造函数的属性options访问到 demo 中 aa 组件的选项对象。那么执行var opts = vm.$options = Object.create(vm.constructor.options)就可以用vm.$options获取到 demo 中 aa 组件的选项对象。

执行opts.parent = options.parent;opts._parentVnode = parentVnode;把之前通过createComponentInstanceForVnode函数传入的几个参数合并到vm.$options

  • vm.$options.parentVnode: 要渲染的组件生成的 Virtual DOM。
  • vm.$options.parent: 要渲染的组件的父 Vue 实例,也就是上下文环境。

合并options完毕后回到this._init中执行if (vm.$options.el){vm.$mount(vm.$options.el)},由于组件选项对象中没有el,在这里不执行vm.$mount挂载。

回到componentVNodeHooks.init钩子函数中,执行child.$mount(hydrating ? vnode.elm : undefined, hydrating) ,这里不是服务端,故hydratingfalse,相当于执行child.$mount(undefined, false),对组件进行挂载,在此打个断点,按F11进入child.$mount方法中。

3、child.$mount

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el, hydrating) {
    var options = this.$options;
    if (!options.render) {
        var template = options.template;
        if (template) {
            if (typeof template === 'string') {
                if (template.charAt(0) === '#') {
                    //...
                }
            } else if (template.nodeType) {
                template = template.innerHTML;
            } else {
                //...
            }
        } 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)
};

initInternalComponent方法中,把组件选项对象合并到this.$options,执行var template = options.templatetemplate<p>{{aa}}<span>{{bb}}</span></p>。因options.render为 undefined ,故调用compileToFunctions方法把template转成成 render 方法,并挂载到this.$options上。

执行return mount.call(this, el, hydrating)在此处打个断点,按F11进入mount方法

Vue.prototype.$mount = function (el,hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

这里要注意此时的el为 undefined。执行return mountComponent(this, el, hydrating)在此处打个断点,按F11进入mountComponent方法

4、mountComponent

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);
    hydrating = false;

    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
    }
    return vm
}

此时vm代表的是组件实例,不是Vue实例。另外此时el为 undefined,故vm.$el为 undefined,这个要记住后面过程中会用到

实例化一个渲染Watcher,初始化的时候会执行回调函数,即执行vm._update(vm._render(), hydrating),按F11进入vm._render方法。

5、vm._render

Vue.prototype._render = function() {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentVnode = ref._parentVnode;
    vm.$vnode = _parentVnode;
    var vnode;
    vnode = render.call(vm._renderProxy, vm.$createElement);
    vnode.parent = _parentVnode;
    return vnode
};

vm.$vnode表示 Vue 实例的父 Virtual DOM,其值vm.$options._parentVnode是在执行createComponentInstanceForVnode(vnode, parent)时,内部有段代码var options = { _parentVnode: vnode,},再通过initInternalComponent合并到vm.$options上,其中的vnode是当前要渲染的组件生成的 Virtual DOM,那么相对于组件的内容就是父 Virtual DOM,也可以叫作组件实例的父 Virtual DOM,如下图所示。

此时的 render 方法,如下所示

(function anonymous() {
    with(this) {
        return _c('div', [_c('p', [_v(_s(aa)), _c('span', [_v(_s(bb))])])])
    }
})

执行vnode = render.call(vm._renderProxy, vm.$createElement),把组件的内容生成vnode(Virtual DOM 树,其过程可以看上一篇文章

执行vnode.parent = _parentVnode把组件内容的父 Virtual DOM,赋值给vnode.parent,最后返回vnode,如下图所示。

按F11进入vm._update方法。

6、vm._update

Vue.prototype._update = function(vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    if (!prevVnode) {
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    } else {
        vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
};

首次渲染,执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)注意此时,传入vm.__patch__的参数vm.$el是 undefined 。最终是调用patch方法,在此处打个断点,按F11进入patch方法中

7、patch

function patch(oldVnode, vnode, hydrating, removeOnly) {
    var isInitialPatch = false;
    var insertedVnodeQueue = [];
    if (isUndef(oldVnode)) {
        isInitialPatch = true;
        createElm(vnode, insertedVnodeQueue);
    } else {

    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
}

oldVnode 为 undefined,故执行createElm(vnode, insertedVnodeQueue),在此处打个断点,按F11进入createElm方法。

8、createElm

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    var children = vnode.children;
    var tag = vnode.tag;
    if (isDef(tag)) {
        vnode.elm = nodeOps.createElement(tag, vnode);
        createChildren(vnode, children, insertedVnodeQueue);
        insert(parentElm, vnode.elm, refElm);
    } else if (isTrue(vnode.isComment)) {
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.text);
        insert(parentElm, vnode.elm, refElm);
    }
}

createElm方法用来创建真实的 DOM 节点,并插入对应的父节点。详解介绍可以上看上一篇内容。

执行createChildren(vnode, children, insertedVnodeQueue),按F11进入createChildren方法。

9、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)));
    }
}

createChildren的逻辑很简单,实际上是遍历vnode子 Virtual DOM,递归调用createElm,这是一种常用的深度优先的遍历算法,在遍历过程中会把vnode.elm作为 的 children[i](Virtual DOM)对应真实 DOM 的父节点传入。

children不是数组时。判断vnode.text是否是基础类型,若是调用nodeOps.createTextNode生成一个纯文本节点,再调用nodeOps.appendChild插入到vnode.elm中。

递归调用createElm中如果当前已经没有子 Virtual DOM,执行insert(parentElm, vnode.elm, refElm)把生成的 DOM (vnode.elm) 插入到对应父节点(parentElm)中,因为是递归调用,子 Virtual DOM 会优先调用insert,所以整个 Virtual DOM 树生成真实 DOM 后的插入顺序是先子后父。 insert(parentElm, vnode.elm, refElm)处打个断点,按F11进入insert方法

10、insert

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);
        }
    }
}
  • 参数parent:要插入节点的父节点
  • 参数elm: 要插入节点
  • 参数ref$$1:参考节点,会在参考节点前插入

nodeOps.insertBefore对应insertBefore方法,nodeOps.appendChild对应appendChild方法,

function insertBefore (parentNode, newNode, referenceNode) {
    parentNode.insertBefore(newNode, referenceNode);
}
function appendChild (node, child) {
    node.appendChild(child);
}

insertBefore方法和appendChild方法其实就是调用原生 DOM 的 API 进行 DOM 操作。

11、回到createElm

等遍历完vnode所有子 Virtual DOM,执行insert(parentElm, vnode.elm, refElm)时,因为在patch中是这么调用createElm,执行createElm(vnode, insertedVnodeQueue),只传递两个参数,故parentElm 为 undefined,那么生成真实的 DOM 树要怎么插到对应的父节点呢?

7、回到createComponent

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false);
        }
        if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue);
            insert(parentElm, vnode.elm, refElm);
            if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
            }
            return true
        }
    }
}

在执行i(vnode, false)中,执行了var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance),所以vnode.componentInstance有值为当前组件实例。

执行initComponent(vnode, insertedVnodeQueue),按F11进入initComponent方法。

function initComponent(vnode, insertedVnodeQueue) {
    vnode.elm = vnode.componentInstance.$el;
}

vnode.componentInstance.$el是在vm._updat方法中执行vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)vm为当前组件实例。

这里的vnode为组件生成的 Virtual DOM ,不是组件内容生成的 Virtual DOM 。

执行insert(parentElm, vnode.elm, refElm)parentElm为组件的父节点,这里为 body,这样就把组件内容生成的 DOM 树插入到body中。

原文链接:juejin.im

上一篇:@toml-tools/parser
下一篇:@1stg/rollup-config

相关推荐

  • 🚩Vue源码——组件是如何注册的

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 在上一篇 🚩Vue源码——组件...

    1 个月前
  • 🚩Vue源码——如何监听数据变化

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue 是用数据来驱动来生成视图...

    10 天前
  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    7 个月前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    7 个月前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    6 个月前
  • 高性能JavaScript之DOM篇

    本篇文章主要分享一下操作DOM时的一些细节,来提高页面性能。 首先我们来思考以下几个问题。 1.如何获取页面中所有class为div1和div2的div元素。 2.你了解HTMLCollection和...

    1 年前
  • 高性能JavaScript DOM编程(1)

    我们知道,DOM是用于操作XML和HTML文档的应用程序接口,用脚本进行DOM操作的代价很昂贵。有个贴切的比喻,把DOM和JavaScript(这里指ECMScript)各自想象为一个岛屿,它们之间用...

    4 年前
  • 面试还问redux?那我从头手撸源码吧(中间件篇)

    昨天的文章手写了一版redux的核心源码,redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我还是尝试将此部分源码完成。 中间件 react中管理数据的流程是单向的,就是说,从派发动作...

    2 年前
  • 面试官问:如何利用 random 计算 π

    前言 这是基友面试 RingCenter 时被问到的一个题目 表面上考察的是概率论等基础知识,实际可能还会问到事件循环等底层知识,以及 React Fiber 蒙特卡洛法求 π 说蒙特卡洛可能不太理...

    6 个月前
  • 附backbone.js观点存在与插入到DOM元素的EL

    Ben Roberts提出了一个问题:attaching backbone.js views to existing elements vs. inserting el into the DOM,或许...

    3 年前

官方社区

扫码加入 JavaScript 社区