乐趣区

Vue原理Component-源码版-之-挂载组件DOM

写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧

【Vue 原理】Component – 源码版 之 挂载组件 DOM

由这篇文章 从模板到 DOM 的简要流程

我们知道,在生成 VNode 之后,下一步就是根据 VNode 生成 DOM 然后挂载了

在本文开始之前你可以先看 Component – 白话版 先整体了解下 component

现在开始我们的正文

上一篇文章 Component – 创建组件 VNode,我们已经说到了【页面模板解析成 VNode 树】的步骤

那今天就就到了【页面 VNode 生成 DOM 挂载】了

等等,今天说的不是 Component 挂载 DOM 吗?跟页面 Vnode 有什么关系??是啊,component 的挂载肯定是跟着父页面的啊,你自己挂?自挂东南枝吗?

好了,废话不说,马上开始


前言预告

这篇 从模板到 DOM 的简要流程 已经说过下面的步骤

1vm._render 执行得到 页面 VNode

2vm._update 拿到 页面 VNode,会开始 patch,不断比对【旧 VNode 和 刚拿到的新 VNode】

对比完之后,会调用一个 createElm 的方法去创建 DOM,然后插入页面

那现在,我们就从 createElm 这个方法突破,前面的流程跟本内容无关,一律略过

function createElm(vnode, parentElm, refElm) {    

    // 组件需要特殊处理

    if (createComponent(vnode, parentElm, refElm)) return

    ... 正常的标签,需要不断递归子节点调用 createElm,然后生成 DOM,并插入到父节点

}

createElm 的作用就是根据 标签名创建 DOM 节点,然后挂载到父节点中,其中参数如下

parentElm == 父 DOM 节点
refElm == 兄弟 DOM 节点,你插入父节点,可能也要知道插在谁附近不是吗,不能乱插的

然后很明显,createElm 每次掉要给你都会调用【createComponent】去检测这个标签是否是组件

如果是组件,就会去创建这个组件的实例,并且 返回 true,从而不用去执行 createElm 下面的部分


调用组件生命钩子

看下 createComponent

function createComponent(vnode, parentElm, refElm) {    

    var data = vnode.data;    

    var hook = i.hook;    

    var init = i.init;    

    // 调用子组件的 init 方法, init 方法就是 Vue.prototype._init

    if (init) {        

        // 创建子组件的 vm 实例

        init(vnode, parentElm, refElm);        

        // 如果存在组件实例,就是上一步创建成功了

        if (vnode.componentInstance) {return true}
    }
}

有没有好奇 vnode.data.hook.init 是什么吗?

他是每个组件,都会被【注册进外壳节点的钩子函数】,没错,就是下面的钩子,源码


什么是组件生命钩子

没错,这就是那个钩子的源码

var componentVNodeHooks = {init(vnode, parentElm, refElm) {        

        var vm= 

          vnode.componentInstance = 
          createComponentInstanceForVnode(

              vnode,activeInstance, 

              parentElm, refElm

          );        

        // 因为 在 Vue.prototype._init 中,只有 $options 存在 el,才会挂载 dom

        // 这里手动挂载组件
        vm.$mount(vnode.elm);
    }
    ...

}

那么,钩子是什么时候注册的呢?

嗯,在上一篇文章,【创建组件外壳 VNode 的过程中】,然后保存到了外壳节点的 data 上

function createComponent(

    Ctor, data, context, 

    children, tag

) {


    ... 创建组件构造函数
    var hooks = data.hook || (data.hook = {});
    data.hook.init = componentVNodeHooks.init

    ... 创建组件 VNode,并保存组件构造函数 和钩子 等到 vnode 中

}

打印一下实际 VNode,没错,有很多钩子,但是现在只说 init

来吧,仔细看那个 init 钩子源码,你可以看到调用了一个方法

createComponentInstanceForVnode

开始深入探索它 ………

创建组件实例

createComponentInstanceForVnode 函数作用就是给 component【增加定制 options】+【调用组件构造函数】

function createComponentInstanceForVnode(
    vnode, parent, 
    parentElm, refElm

) {    

    // 增加 component 特有 options
    var options = {        

        _isComponent: true,        

        parent: parent, // 父实例

        _parentVnode: vnode, // 外壳节点
        _parentElm: parentElm , // 父 DOM
        _refElm: refElm  // 兄弟 DOM

    };    

    // vnode.components.Ctor 就是 构造函数,里面会调用 Vue.prototype._init

    return new vnode.componentOptions.Ctor(options)
}

vnode.componentOptions.Ctor 就是 构造函数,就是下面这个,上篇文章 Component – 创建组件 VNode 时保存在外壳节点的

function VueComponent(options) {this._init(options); 

}

new 了之后,自然而然,走到了 _init 方法,在 init 方法中,有一个特殊照顾 component 的方法,专门给 component 实例设置 options

“ 这一步跟 挂载组件 DOM 没什么关联,想去掉的,但是想想还是先保留下来,完整整个流程 ”

Vue.prototype._init = function(options) {if (如果是组件) {initInternalComponent(vm, options);
    }
}

组件初始化 initInteralComponent

function initInternalComponent(vm, options) {    

    // 这个 options 就是在创建构造函数时,合并的 options,全局选项和组件设置选项

    var opts = vm.$options = Object.create(vm.constructor.options);        
    
    // 保存父节点,外壳节点,兄弟节点等

    var parentVnode = options._parentVnode; // _parentVnode 是外壳节点
    opts.parent = options.parent; // options.parent 是 父实例
    opts._parentVnode = parentVnode;
    opts._parentElm = options._parentElm;

    opts._refElm = options._refElm;    

    // 保存父组件给子组件关联的数据
    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;
    }
}

这个时候,init 的过程就完成了

下一步就是到了 mount 过程


组件解析模板并挂载

可以再回看下「componentVNodeHooks.init」那个钩子源码

在创建组件实例成功之后,会手动调用实例 vm.$mount 进行挂载,就是这句代码完成的功能

然而,挂载的步骤,就是正常标签挂载的步骤了

详情可以查看 从模板到 DOM 的简要流程
的 mount 过程,是一毛一样的,就不多说了


总结

1、父页面已经拿到了 VNode,其中会调用 createElm 根据 VNode 生成 DOM,进行挂载

2、不断的递归遍历子节点,使用 createComponent 判断标签是否是组件

3、遇到组件,拿到组件外壳 VNode 的 data(data 保存有父组件给子组件的,事件,props,构造函数,钩子)

4、从 data 中拿到 hook,hook 中拿到 init 钩子,并执行 init 钩子

5、init 钩子中,调用 createComponentInstanceForVnode 调用组件构造函数,并返回组件

6、init 钩子中,使用上一步返回的实例,手动调用 vm.$mount 进行组件内部模板解析渲染,并挂载

退出移动版