【手写 Vue2.x 源码】第四十二篇 - 组件部分 - 组件挂载流程简述

281 阅读2分钟

一,前言

上篇,组件部分-生成组件的真实节点;

本篇,组件部分-组件挂载流程分析;


二,组件挂载流程分析

1,示例

  • 全局组件:my-button,name:'全局组件';
  • 局部组件:my-button;name:'局部组件';
<body>
  <div id="app">
    <my-button></my-button>
  </div>
  <script src="./vue.js"></script>
  
  <script>
    // 全局组件
    Vue.component('my-button',{
      name:'my-button',
      template:'<button>Hello Vue {{name}}</button>',
      data(){
        return { name: '全局组件'}
      }
    })
    
    // 局部组件
    new Vue({
      el: "#app",
      components:{ 
        'my-button':{
          template:'<button>Hello Vue {{name}}</button>',
          data(){
            return { name: '局部组件'}
          }
        }
      }
    });
  </script>

2,组件的挂载流程

// src/init.js

export function initMixin(Vue) {
  // 初始化
  Vue.prototype._init = function (options) {
    const vm = this; 
    
    vm.$options = mergeOptions(vm.constructor.options, options);
    initState(vm);

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

  Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el);
    vm.$el = el;

    if (!opts.render) {
      let template = opts.template;
      if (!template) {
        template = el.outerHTML;
      }
      let render = compileToFunction(template);
      opts.render = render;
    }

    mountComponent(vm);
  }

  Vue.prototype.$nextTick = nextTick;
}

第一次进入,为根节点初始化,el#app

image.png

第二次进入,组件的初始化,elundefined

  • 继续,进行组件的挂载操作 mountComponent
  • 组件的挂载:核心是创造出一个组件的虚拟节点,并调用组件的更新方法 _update
  • render.call 创造组件的虚拟节点 vnode,即 button 的虚拟节点:
  • vm.render 执行完成后,继续执行 _update 方法:
  • 这时,不能获取到上一次的虚拟节点:
  • 当组件挂载时,$patch 方法中的 elnull
  • 此时,patch 内部判断了,如果 oldVnodenull,就使用组件的虚拟节点,创建出组件的真实节点:
  • 此时,返回的 vm.$el 就是 button 内部包含着内容:

此时,子组件就挂载完毕了!

  • vnode.componentInstance 存在,则说明是组件,createComponent 方法返回 true:
  • createElm 方法中,返回组件的 $el,即组件对应的真实节点 button
  • createElm 递归处理,将生成的组件真实节点,放到对应的父节点上;
  • 再将完整的 div 挂载到页面上;

三,完整流程

1,实现了 Vue.component,它的核心功能是注册成全局组件;

内部会自动调用 Vue.extend 方法,返回组件的构造函数;

2,在组件初始化时,会进行组件的合并 mergeOptions

mergeOptions:优先查找局部组件,若找不到,则继续通过链向上查找全局组件;

3,组件合并完成之后,内部会对模板进行编译操作,最终会走到 _c('组件名')

进行标签筛查:

  • 如果是组件,创建组件虚拟节点;在组件的虚拟节点中,最重要的就是 componentOptions,里面存放了组件的所有内容(属性的实现、事件的实现、插槽的内容、Ctor);
  • 而且,还会判断 Ctor,如果是对象,会调用 Vue.extend(componentOptions) 处理为组件的构造函数;

所以,所有的组件都是通过 Vue.extend 方法来实现的;

4,创建组件的虚拟节点

  • create 执行完成后,就该走 patch 了,在 patch 方法中,有一个非常重要的createElm 方法:将虚拟节点转化为真实节点;
  • 在组件生命周期 hook:init 中,通过 let child = new Ctor({}) 得到组件实例 child
  • new 子类 时,会调用子类的初始化 _init,同时,会把子类的选项和它的选项进行合并,并且对组件中的 data 数据进行初始化,劫持数据做响应式处理,由于没有传入 el,所以不会进行挂载
  • 接下来,组件实例调用 $mount() 方法 child.$mount():入参 el 为空,由于 vm.$el = el 所以 $el 为空,对组件进行模板编译生成组件的 render 函数并缓存的到 opts.render 上,并执行组件的挂载 mountComponent,相当于重新走了一遍 new Vue,构建了一个组件的实例;
  • 这时,又会去走组件的 beforeCreate 生命周期 hook,再去给组件创建一个 watcher

由于每个组件都会走一遍初始化流程,所以,每个组件都会有一个 watcher

  • 有了 watcher 就会走子组件渲染,根据子组件创建一个虚拟节点vm._render() 返回 vnode

5,根据虚拟节点生成真实节点

  • 通过 vm._update 对组件进行初始化渲染,赋值给 vm.el,生成组件的真实节点el,生成组件的真实节点 `el`,对应组件模板渲染后的结果;(第一次是 id=app,第二次是 null,因为 $mount 中没有传参 el,此时 patch 中的 oldVnode 为 null,此时说明是组件,创建出组件的真实节点)
  • 通过 vnode.componentInstance = new Ctor() 使后面可以拿到组件渲染后的结果: vnode.componentInstance.$el

6,将组件的 vnode.componentInstance.$el 插入到父标签中

7,在 new Ctor() 组件实例化时,会执行组件初始化流程,为组件添加独立的渲染过程;

  • 每个组件实例都拥有独立的渲染 watcher
  • 当组件渲染时,组件对应的属性会收集自己的渲染 watcher
  • 当组件更新时,只需更新组件对应的渲染 watcher 即可;
  • 所以,组件是局部更新的,性能也会比较好

四,结尾

本篇,组件部分-组件挂载流程简述;

下篇,组件部分-组件相关流程总结;

备注:还需要添加很多图,后续迭代。。。