实现组件
vue的diff算法
Vue中,一般一个项目只有一个根组件,也就是 new Vue产生的app。
但是一个页面不可能只由一个组件构成,很明显我们需要实现自定义组件。
vue中提供了两种自定义组件的方式:
- 全局组件
- 局部组件
组件的使用流程:
在任意一个组件中,都可以使用其他组件。当我们在一个组件中使用其他组件的时候,会先去组件内部的局部组件中找是否定义过该组件,如果定义了,则直接使用该局部组件;如果没有定义局部组件,则去全局组件中寻找(和js中的原型,原型链很像了)。所以vue内部很可能也是利用类似于继承的这种模型实现组件的定义的。
其实vue内部在定义组件的时候,表面上我们是传递了一个对象:
Vue.component("cmp",{
//...
})
实际上这个对象内部也会被Vue.extend给包裹,变成子类
.
Vue.component("cmp",Vue.extend({
//...
}))
组件的三大特性
- 自定义标签
- 组件有自己的属性和事件
- 组件的插槽
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;
};
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和合并选项的部分代码实现:
**不过这样还是有一些小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;
};
实现了组件的寻找规则,接下来只需要在组件的模板解析时,去寻找组件并渲染子组件。
之前我们都是模板生成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:{}
})
我们在实例化这个返回的子类的时候,也就是 new Sub,会调用父亲Vue上的_init方法,然后在该方法的内部,又会进行mergeOptions合并选项的操作。也就是每次合并选项,都会把子类上的options都拿一份放到实例自己的$options上。如果data是一个对象,那么每次都会把data的引用放到实例对象自己身上。
多个子类实例会共享一个Sub上的options.data。但是如果data是一个函数,我们虽然也是直接把data放到实例对象的身上,但是在初始化属性拦截数据的时候,发现data是一个函数的情况下,我们会执行这个函数,拿到真正的data数据。每次执行函数返回的都是一个全新的对象,哪怕每个对象的所有属性都一样,但是他们直接不会相互影响。
在创建子类的构造函数的时候,会把全局的组件和自己身上定义的组件进行合并(组件的合并规则,先找自己身上是否有该组件,没有的情况下,然后去全局查找)
组件的渲染:
开始渲染的组件会编译组件的模板,变成render函数。然后调用render方法。
createElementVNode会根据tag类型来区分否是普通节点和组件节点。
对于组件节点:我们在创建的时候,会给一个标识,包含组件的构造函数。且在data中增加一个初始化的init钩子。
稍后在创建组件对应的真实节点的时候,只需要new Ctor即可。
创建真实节点:
在创建真实节点的时候,也就是在createEle方法内部,我们可以调用createComponent方法来创建组件。如果是组件,当然就会调用上面创建组件的虚拟节点的时候,插入的init的hook。然后返回组件生成的$el;不是组件当然也无伤大雅,会不满足组件的条件,正常往普通组件的流程往下走。
function createComponent(vnode) {
// init 初始化组件
vnode.props?.hook?.init(vnode);
return vnode.componentInstance;
}
所以到此为止,就实现了组件的渲染流程。