前言
我们在前面讨论过了vue渲染器,渲染器主要负责将虚拟 DOM 渲染为真实 DOM ,我们只需要使用虚拟 DOM来描述最终呈现的内容即可。
但当我们编写比较复杂的页面时,用来描述页面结构的虚拟 DOM 的代码量会变得越来越多,或者说页面模板会变得越来越大。这时,我们就需要组件化的能力。
有了组件,我们就可以将一个大的页面拆分为多个部分,每一个部分都可以作为单独的组件,这些组件共同组成完整的页面。组件化的实现同样需要渲染器的支持
渲染组件
从用户的角度来看,一个有状态组件就是一个选项对象,如下面的代码所示:
// MyComponent 是一个组件,它的值是一个选项对象
const MyComponent = {
name: 'MyComponent',
data(){
return {foo: 1 }
}
}
但是,如果从渲染器的内部实现来看,一个组件则是一个特殊类型的虚拟 DOM 节点。例如,为了描述普通标签,我们用虚拟节点的vnode.type 属性来存储标签名称,如下面的代码所示:
// 该 vnode 用来描述普通标签
const vnode = {
type: 'div'
// ...
}
为了描述片段,我们让虚拟节点的 vnode.type 属性的值为Fragment, 而为了描述文本,我们让虚拟节点的 vnode.type 属性的值为Text,
还记得我们之前在渲染器讨论过的patch函数
吗?
在patch函数中,Vue会根据新旧虚拟DOM节点的差异进行不同的处理逻辑。 如果是首次渲染,会执行首次渲染的逻辑;如果是更新渲染,会根据差异更新实际的DOM节点。
function patch(n1, n2, container, anchor) {
// 02. 如果新旧节点类型不同,执行卸载操作
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
// 07. 获取节点类型
const { type } = n2;
// 09. 判断节点类型
if (typeof type === 'string') {
// 10. 处理普通元素
// TODO: 处理普通元素的逻辑
} else if (type === Text) {
// 11. 处理文本节点
// TODO: 处理文本节点的逻辑
} else if (type === Fragment) {
// 13. 处理片段
// TODO: 处理片段的逻辑
}
// 返回新节点的实际DOM元素
return n2.el;
}
可以看到,渲染器会使用虚拟节点的 type 属性来区分其类型。对于不同类型的节点,需要采用不同的处理方法来完成挂载和更新。
实际上,对于组件来说也是一样的。为了使用虚拟节点来描述组件,我们可以用虚拟节点的 vnode.type 属性来存储组件的选项对象
function patch(n1, n2, container, anchor) {
// 02. 如果新旧节点类型不同,执行卸载操作
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
// 07. 获取节点类型
const type = n2.type;
// 09. 判断节点类型
if (typeof type === 'string') {
// 10. 处理普通元素
// TODO: 处理普通元素的逻辑
} else if (type === Text) {
// 11. 处理文本节点
// TODO: 处理文本节点的逻辑
} else if (type === Fragment) {
// 13. 处理片段
// TODO: 处理片段的逻辑
} else if (typeof type === 'object') {
// 16. vnode.type 的值是选项对象,作为组件来处理
if (!n1) {
// 17. 挂载组件
mountComponent(n2, container, anchor);
} else {
// 21. 更新组件
patchComponent(n1, n2, anchor);
}
}
// 返回新节点的实际DOM元素
return n2.el;
}
在上面这段代码中,我们新增了一个 else if 分支,用来处理虚拟节点的 vnode.type 属性值为对象的情况,即将该虚拟节点作为组件的描述来看待,并调用 mountComponent 和 patchComponent 函数来完成组件的挂载和更新。
上面我们回顾了有关于组件的一些基本概念,组件的挂载和更新是如何实现的,其实我们更关注的还应该是用户应该如何编写组件?组件的选项对象必须包含哪些内容?以及组件拥有哪些能力?
因此,一个组件必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM。换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口,如下面的代码所示:
// MyComponent 组件定义
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 组件的渲染函数,其返回值必须为虚拟 DOM,一个组件必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM
render() {
// 返回虚拟 DOM
return {
type: 'div',
children: ['text content']
};
}
};
// 用来描述组件的 vnode 对象,type 属性值为组件的选项对象
const CompNode = {
type: MyComponent
};
// 调用渲染器来渲染组件
renderer.render(CompNode, document.querySelector('#app'));
// 渲染器中真正完成组件渲染任务的是 mountComponent 函数
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type;
// 获取组件的渲染函数 render
const render = componentOptions.render;
// 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
const subTree = render();
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor);
}
总结
我们现在来回顾一下组件的基础,我们显式声明了一个组件的对象实例,该组件的选项对象必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM,之后我们再调用render和patch进行组件的更新和替换。换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口,而渲染器的作用就是将组件的渲染函数的返回值渲染为真实 DOM。
評論