读Vue2源码(4)-组件_render

88 阅读3分钟

组件案例-全局注册

<body>
  <div id="components-demo">
    <button-counter></button-counter>
  </div>
</body>
<script src="vue.js"></script>
<script>
// 全局注册
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
var app = new Vue({ el: '#components-demo' })
</script>

组件案例-局部注册

<!DOCTYPE html>
<body>
  <div id="components-demo">
    <button-counter></button-counter>
  </div>
</body>
<script src="vue.js"></script>
<script>
// 局部注册
var ButtonCounter = {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
}
var app = new Vue({
  el: '#components-demo',
  components: {
    'button-counter': ButtonCounter
  },
})
</script>

转换后的 render

vm._render 方法是将 render 转换成 vnode,当前案例我们得到的 render 是:

ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"components-demo"}},[_c('button-counter')],1)}
}

当我们执行 _c('div',{attrs:{"id":"components-demo"}},[_c('button-counter')],1) 时,会先执行参数中的 _c('button-counter')也就是会先将子元素的 render 转换为 vnode,再转换父元素的 render

组件转换为 vnode

在 createElement 实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode

if (config.isReservedTag(tag)) {
    //  html 标签
    vnode = new VNode(...);
} else if (...) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag);
}

全局注册的 Ctor

全局注册的组件中,Ctor 是一个函数,这个函数上添加了很多属性,可以看出对于单个组件都会重新走一遍 _init 的初始化方法

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

image.png

局部注册的 Ctor

局部注册时,Ctor 是一个对象。

{
    data: () { return { count: 0 } }
    template: "<button v-on:click=\"count++\">You clicked me {{ count }} times.</button>"
}

createComponent

构造子类构造函数

var baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
   Ctor = baseCtor.extend(Ctor);
}

baseCtor 就是我们开始看到的 Vue 函数。

function Vue(options) {
   if (!(this instanceof Vue)) {
      warn$2('Vue is a constructor and should be called with the `new` keyword');
   }
   this._init(options);
}

image.png

当我们使用局部注册的方式注册组件的时候会走到 isObject(Ctor) 中的逻辑,从而执行 baseCtor.extend(Ctor) 方法,这里使用的是 Vue.extend 方法。

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 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;
          ...
          // 缓存 Sub
          cachedCtors[SuperId] = Sub;
          return Sub;
      };

Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

实现继承后,局部注册时的 Ctor 对象长这样:

{
    data: () { return { count: 0 } }
    template: "<button v-on:click=\"count++\">You clicked me {{ count }} times.</button>"
    _Ctor: {0: VueComponent(options) { this._init(options);}}
}

安装组件钩子函数

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];
          // @ts-expect-error
          if (existing !== toMerge && !(existing && existing._merged)) {
              hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
          }
      }
  }

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数(destroy,init,insert,prepatch)合并到 data.hook 中,VNode 执行 patch 的过程中(将 vnode 转换为真实 Dom )执行相关的钩子函数。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。

实例化 vnode

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

最后一步通过 new VNode 实例化一个 vnode 并返回。需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的,这点很关键。这里创建的 vnodetag 为:vue-component-1-button-counter

案例解析

_c('button-counter') 生成一个 tag 为 vue-component-1-button-counter 的空的 vnode。 _c('div',{attrs:{"id":"components-demo"}},[_c('button-counter')],1) 创建了一个 tag 为 div,包含属性 id 为 'components-demo',children 为 vue-component-1-button-counter 这个空的 vnode。 再去初始化子节点,_c('button',{on:{"click":function($event){count++}}},[_v("You clicked me "+_s(count)+" times.")]) 生成的是一个 tag 为 button,属性含有 click 方法,children 是text为 You clicked me 0 times. 的 vnode。

ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"components-demo"}},[_c('button-counter')],1)}
}
ƒ anonymous(
) {
with(this){return _c('button',{on:{"click":function($event){count++}}},[_v("You clicked me "+_s(count)+" times.")])}
}