Vue2核心原理(简易版)-组件初始化流程

547 阅读1分钟

Vue2核心原理(简易版)-组件初始化流程

什么是vue组件?

组件这部分内容十分复杂,单讲几句话是描述不清楚的。在我们这篇文章中你所需要知道的是:1. 组件是可复用的 Vue 实例;2. 组件的注册类型分为全局注册和局部注册。(全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。)

示例:

// 全局注册
Vue.component('my-button1', {
  template: `<button class="my-button">全局组件按钮</button>`
})
// 局部注册
// 不通过.vue文件创造的组件MyButton2
const MyButton2 = Vue.extend({
  name: 'MyButton2',
  template: `<button onclick="this.toggleMessage" class="my-button2">局部组件按钮</button>`,
}) 
let vm = new Vue({
  el: '#app',
  component: {
    'my-button2': MyButton2
  }
})

如图:

实现思路

组件初始化的过程

  1. 在之前,不论是我们的data,watch,computed...这些出现在options上的属性,我们都会习惯性的在initState中,对他们进行init操作的:
    data是这样的:

    // 初始化data
    function initData() {
      const vm = this;
      let data = vm.$options.data;
      data = vm._data = typeof data === "function" ? data.call(vm) : data;
      // 代理到vm上
      for (let key in data) {
        proxy(vm, "_data", key);
      }
      observe(data);
    }
    

    watch是这个姿势:

    // 初始化watch
    function initWatch() {
      const vm = this;
      const watch = vm.$options.watch;
      Object.keys(watch).forEach((key) => {
        let handler = watch[key];
        if (Array.isArray(handler)) {
          for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i]);
          }
        } else {
          createWatcher(vm, key, handler);
        }
      });
    }
    

    computed又是如此这般:

    // 初始化computed
    function initComputed() {
      const vm = this;
      const computed = vm.$options.computed;
      vm._computedWatchers = {};
      for (let key in computed) {
        const userDef = computed[key];
        let getter = typeof userDef === "function" ? userDef : userDef.get;
    
        // 每一个计算属性实际就是一个watcher
        // 因为计算属性默认是不执行的,所以要给一个lazy true
        vm._computedWatchers[key] = new Watcher(vm, getter, () => {}, {
          lazy: true,
        });
        // 将key定义在vm上,否则是取不到的
        defineComputed(vm, key, userDef);
      }
    }
    
    • 那么我们的新朋友,components呢,乍一看,局部注册的components和我们的老朋友们也一样出现在options配置当中,理论上我们也需要对其进行一步initComponent操作。
    • 然而我们的确并没有,或者说,其实我们没有这个需要,为什么呢?因为components对象下,我们所定义的'my-button2': MyButton2里面的MyButton2实质上已经完成了我们对components这个属性的预期,或者说是对'my-button2'这个新注册的组件的预期。因为我们仅仅是希望,在我们模版编译的过程中,能找到'my-button2'VueComponent构造而已,嘿你说巧不巧,MyButton2就正好满足我们的要求,我们可以直接拿来用。

    明确两点或许对你理解上面两段话有点儿帮助!

    组件其实就是可复用的特殊的 Vue 实例
    在生成真实节点的时候,遇到组件节点时,实际上就是把它看作一个新的Vue实例,对它再继续进行模版编译->构建vdom->生成真实dom。

    i. 怎么把我们写的这种配置转成你说的VueComponent构造呢?
    ii. 我们是怎么在生成真实节点的时候,找到VueComponent构造,生成vdom节点,并且对它进行挂载的?

    针对问题一,我们可以提供一个全局api方法,Vue.extend:

    Vue.extend = function (opts) {
     // 原型继承
     const Super = this; // 父亲大Vue
     const Sub = function VueComponent(options) {
       this._init(options); // 原型继承 this._init需要写入子类才会执行
     };
     Sub.prototype = Object.create(Super.prototype);
     Sub.prototype.constructor = Sub; // 构造函数指向自己
     Sub.options = mergeOptions(Super.options, opts); // 先把extend接收到的options合并到Sub上去,后面实例化的时候还会再合并
     return Sub;
    };
    

    看懂上面这个extend方法需要了解我们大名鼎鼎的JavaScript的原型继承机制,这部分内容不在我们今天的讨论范围,但是,我们可以粗略的理解为上面的代码就干了下面的事情。

    1. 继承大Vue,成为它的一个子类,具有大Vue原型上所有的功能。
    2. extend会首先接收一个对象options,这个对象options上的配置属性会首先被merge到我们所要构造的VueComponent这个子类的options上去,随后当子类被实例化的过程中,新的配置又会再次merge到options上去,最后返回这个VueComponent构造。(this._init中有此操作,可以翻看代码)

    所以我们的局部注册组件:

    const MyButton2 = Vue.extend({
     name: 'MyButton2',
     template: `<button onclick="this.toggleMessage" class="my-button2">局部组件按钮</button>`,
    }) 
    

    实际上是这个样子的:

    OK,现在局部注册的时候我们用Vue.extend可以把那个对象变成一个VueComponent构造了。但是全局注册怎么办呢?我们只需要也让对象被extend一下就好了!

    Vue.component = function (id, definition) {
     // definition是一个对象
     // 通过this.options._base找到大Vue,保证具有全局api
     // 保证组件间的隔离,每个组件都会产生一个新的类,去继承父类
     definition = this.options._base.extend(definition);
     this.options.components[id] = definition;
    };
    

    看到了吧,定义在Vue.component的那个对象(definition),最后还是免不了被Vue.extend生成VueComponent构造,然后添加到this.options.components的命运。

组件挂载的过程

  1. 首先,组件和普通节点的分水岭就在这里,createElement,就是生成虚拟节点的时候。

    export function createElement(vm, tag, data = {}, ...children) {
      // 这里要开始区分原始标签还是组件标签了
      if (!isReservedTag(tag)) {
        const Ctor = vm.$options.components[tag]; // 找到此组件名的构造函数,要么找到局部注册的,要么就是全局的
        return createComponent(vm, tag, data, data.key, children, Ctor);
      } else {
        return vnode(vm, tag, data, data.key, children, undefined);
      }
    }
    
    function createComponent(vm, tag, data, key, children, Ctor) {
     // 组件的构造函数
     if (isObject(Ctor)) {
       Ctor = vm.$options._base.extend(Ctor); // Vue.extend
     }
     data.hook = {
       // 等会渲染组件时 需要调用此初始化方法
       init(vnode) {
         let vm = (vnode.componentInstance = new Ctor({ _isComponent: true })); // new Sub 会用此选项和组件的配置进行合并
         vm.$mount(); // 组件挂载完成后 会在 vnode.componentInstance.$el => <button>
       },
     };
     return vnode(vm, `vue-component-${tag}`, data, key, undefined, undefined, {
       Ctor,
       children,
     });
    }
    

    我们会把标签名变成这个样子vue-component-${tag},当然,最重要的部分就是找到这个组件的Ctor(const Ctor = vm.$options.components[tag]),然后把它放到vnode中去。这样子,在我们后面的patch过程中,才可以生成组件的实例,并且执行$mount,生成真的$el(真实dom节点)。

    function createComponent(vnode) {
      let i = vnode.data; //  vnode.data.hook.init
      if ((i = i.hook) && (i = i.init)) {
        i(vnode); // 调用init方法
      }
      if (vnode.componentInstance) {
        // 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
        return true;
      }
    }
    

    大功告成!

小插曲-component的mergeOptions

  • 如果真的细细品读, 你可能会发现,const Ctor = vm.$options.components[tag];Ctor是在vm.$options.components上面找的,但是全局注册的时候,我们没有把全局的这个构造放到vm.$options.components上面去,那么这究竟是怎么一回事呢?

    // 让组件options的合并成为原型继承关系,局部组件没有就去找全局组件
    strats.components = function (parentVal, childVal) {
      // Vue.options.components
      let options = Object.create(parentVal); // 根据父对象构造一个新对象 options.__proto__= parentVal
      if (childVal) {
        for (let key in childVal) {
          options[key] = childVal[key];
        }
      }
      return options;
    };
    

    我们是在mergeOptions的时候动了手脚,让局部的components能沿着原型链找到全局定义在Vue.options上的组件构造!

    所以,它们的关系就正如下图所示

原文链接

如果你在非掘金地址看到这篇文章,这里有原文传送门,您的点赞是对我最大的支持!

完 🎉