mini-vue之组件的实现和渲染流程 以及局部和全局组件建立的联系

实现组件

vue的diff算法
Vue中,一般一个项目只有一个根组件,也就是 new Vue产生的app。

但是一个页面不可能只由一个组件构成,很明显我们需要实现自定义组件。

vue中提供了两种自定义组件的方式:

  1. 全局组件
  2. 局部组件

组件的使用流程:

在任意一个组件中,都可以使用其他组件。当我们在一个组件中使用其他组件的时候,会先去组件内部的局部组件中找是否定义过该组件,如果定义了,则直接使用该局部组件;如果没有定义局部组件,则去全局组件中寻找(和js中的原型,原型链很像了)。所以vue内部很可能也是利用类似于继承的这种模型实现组件的定义的。

其实vue内部在定义组件的时候,表面上我们是传递了一个对象:

Vue.component("cmp",{
    //...
})

实际上这个对象内部也会被Vue.extend给包裹,变成子类.

Vue.component("cmp",Vue.extend({
    //...
}))

组件的三大特性

  1. 自定义标签
  2. 组件有自己的属性和事件
  3. 组件的插槽

Vue.extend的实现

既然组件的实现内部还是需要调用extend方法,那么就先把extend实现出来。

**用法:**使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

实现:

这个实现就不难了:不过就是实现一个构造函数,让该函数继承Vue而已。就是组合式继承。

/**
   * 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
   * 返回值是一个构造函数 通过new可以创建一个vue组件实例
   * @param {{data:Function,el:string}} options
   * @returns
   */
Vue.extend = function (options) {
    // 组合式继承 Vue
    function Sub(options = {}) {
        // 最终使用的组件 就是 new 一个实例
        this._init(options);
    }
    Sub.prototype = Object.create(Vue.prototype);
    Object.defineProperty(Sub.prototype, "constructor", {
        value: Sub,
        writable: true,
        configurable: true,
    });
    Sub.options = options; // 保存用户传递的选项
    return Sub;
};

image-20220417223435741

Vue.component实现

参数:

  • id: string
  • definition?: Function | object

**用法:**注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')

实现:

// 维护一个 全局组件对象
  Vue.options.components = {};
  /**
   * 定义或者获取全局组件 没有获取到组件时 返回 undefined
   * @param {string} id
   * @param {Function | object} definition
   */
  Vue.component = function component(id, definition) {
    // 获取全局组件
    if (!definition) return Vue.options[id];
    // 如果 definition 是一个函数,说明用户自己调用了 Vue.extend
    // 不是函数 就用 extend函数包装一下
    !isFunction(definition) && (definition = Vue.extend(definition));
    Vue.options.components[id] = definition;
  };

实现全局的组件注册并不难,其核心还是利用了extend方法。

全局component和局部component

对于一个组件中,我们如果使用了一个其他组件,且在全局和局部都注册了一个同名的组件,那么我们会优先使用哪个?vue中会优先使用组件内部注册的局部组件。

我们在处理创建组件时的配置的时候,要维护一下:components:{"btn":{}}.__proto__ -> Vue.options.components

const Cmp = Vue.extend({
    template: `<div>
<h2>你好!{{name}}</h2>
<btn/>
</div>`,
    components:{
        btn:{
            template:`<button>局部button</button>`
        }
    }
});
Vue.component("btn",{
    template:`<button>全局button</button>`
})
const cmp = new Cmp({
    data: {
        name: "张三"
    }
})
cmp.$mount("#app")

我们需要修改一下当时extend和合并选项的部分代码实现:

image-20220417233341024

image-20220417233534097

**不过这样还是有一些小bug,我觉得这样实现就更加完美了。**不过在vue中的实现方式还是上面那种。

把合并策略再次修改一下:

strategy.components = function (parentVal, childVal) {
  // 已经和全局组件对象创建关系了,则不需要再次建立关系 直接返回
  if (Object.getPrototypeOf(parentVal) === Vue.options.components)
    return parentVal;
  // 通过父亲 创建一个对象 原型上有父亲的所有属性和方法
  const res = Object.create(parentVal); // {}.__proto__ = parentVal
  if (childVal) {
    for (const key in childVal) {
      // 拿到所有的孩子的属性和方法
      res[key] = childVal[key];
    }
  }
  return res;
};

image-20220417235410784

实现了组件的寻找规则,接下来只需要在组件的模板解析时,去寻找组件并渲染子组件。

之前我们都是模板生成ast以后,然后生成虚拟dom,下一步就是比对节点生成真实dom了。

但是当我们引入组件以后,就需要对元素再次分类,分类出组件的虚拟节点和其他的普通节点。

我们需要在生成vnode的时候,判断出该标签是原始标签还是自定义组件的标签。

一个朴素无华的操作就是判断此tag是否是所有原始标签的一种。。。

const ReservedTags = [
  "div",
  "h1",
  "h2",
  "h3",
  "h4",
  "h5",
  "h6",
  "span",
  "ul",
  "ol",
  "li",
  "a",
  "table",
  "button",
  "input",
];

const isReservedTag = (tag) => {
  return ReservedTags.includes(tag);
};
渲染流程

Vue.component的作用就是进行组件的全局定义而已。把id和definition对应。让 Vue.options.componnets[id] = definition。只是如果definition是对象的情况下,会帮我们使用extend进行包裹成构造函数(Vue子类)。

  • Vue.extend返回值就是一个Vue子类,一个继承了父类Vue的构造函数。(为什么Vue的组件中的data不能是一个对象呢?)
Vue.extend({
    data:{}
})

image-20220418124139726

我们在实例化这个返回的子类的时候,也就是 new Sub,会调用父亲Vue上的_init方法,然后在该方法的内部,又会进行mergeOptions合并选项的操作。也就是每次合并选项,都会把子类上的options都拿一份放到实例自己的$options上。如果data是一个对象,那么每次都会把data的引用放到实例对象自己身上。

多个子类实例会共享一个Sub上的options.data。但是如果data是一个函数,我们虽然也是直接把data放到实例对象的身上,但是在初始化属性拦截数据的时候,发现data是一个函数的情况下,我们会执行这个函数,拿到真正的data数据。每次执行函数返回的都是一个全新的对象,哪怕每个对象的所有属性都一样,但是他们直接不会相互影响。

在创建子类的构造函数的时候,会把全局的组件和自己身上定义的组件进行合并(组件的合并规则,先找自己身上是否有该组件,没有的情况下,然后去全局查找)

组件的渲染:

开始渲染的组件会编译组件的模板,变成render函数。然后调用render方法。

createElementVNode会根据tag类型来区分否是普通节点和组件节点。

image-20220418125436483

对于组件节点:我们在创建的时候,会给一个标识,包含组件的构造函数。且在data中增加一个初始化的init钩子。

image-20220418125529930

稍后在创建组件对应的真实节点的时候,只需要new Ctor即可。

创建真实节点:

在创建真实节点的时候,也就是在createEle方法内部,我们可以调用createComponent方法来创建组件。如果是组件,当然就会调用上面创建组件的虚拟节点的时候,插入的init的hook。然后返回组件生成的$el;不是组件当然也无伤大雅,会不满足组件的条件,正常往普通组件的流程往下走。

function createComponent(vnode) {
  // init 初始化组件
  vnode.props?.hook?.init(vnode);
  return vnode.componentInstance;
}

image-20220418135730777

image-20220418141046739

所以到此为止,就实现了组件的渲染流程。

image-20220418141303290

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尤雨东

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值