为何要组件化?
渲染器主要负责将虚拟 DOM 渲染为真实 DOM,我们只需要使用虚拟 DOM 来描述最终呈现的内容即可。但当我们编写比较复杂的页面时,用来描述页面结构的虚拟 DOM 的代码量会变得越来越多,或者说页面模板会变得越来越大。这时,我们就需要组件化的能力。
有了组件,我们就可以将一个大的页面拆分为多个部分,每一个部分都可以作为单独的组件,这些组件共同组成完整的页面。从而降低了页面实现的复杂度。因为页面被拆分了,复杂度被分散到各个组件中。
组件化的实现依赖于渲染器的支持。
Vue3 中组件是什么?
从用户(Vue.js 的使用者)的角度来看,一个有状态组件就是一个选项对象,简单地说,组件其实就是对象。如下代码所示:
js
// MyComponent 是一个组件,它的值是一个选项对象
const MyComponent = {
name: 'MyComponent',
data() {
return { foo: 1 }
}
}
从渲染器(Vue.js 内部)的内部实现来看,组件是特殊类型的虚拟 DOM 节点。渲染器使用虚拟 DOM 的 type 属性区分不同类型的元素。不同类型的元素会有不同的处理逻辑。
js
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// 作为普通元素处理
} else if (type === Text) {
// 作为文本节点处理
} else if (type === Fragment) {
// 作为片段处理
}
}
对于组件来说,其虚拟 DOM 的 type 属性为组件的选项对象
js
// 该 vnode 用来描述组件,type 属性存储组件的选项对象
const vnode = {
type: MyComponent
// ...
}
在 patch 函数中,判断虚拟 DOM 的 type 是组件类型则会调用 mountComponent 或 patchComponent 方法来挂载或更新组件。
👆 上面的代码摘自 Vue.js 3.2.45
设计组件在用户层面的接口
对于一个框架来说,暴露给用户层面的接口是非常重要的,因为它决定了用户使用这个框架的体验。
渲染器有了处理组件的能力后,下一步要做的就是:设计组件在用户层面的接口。这包括:
-
用户应该如何编写组件?
-
组件的选项对象必须包含哪些内容?
-
以及组件拥有哪些能力?
-
其他...
实际上,组件是对页面内容的封装,它用来描述页面内容的一部分。因此,一个组件必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM。换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口,如下面的代码所示:
js
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 组件的渲染函数,其返回值必须为虚拟 DOM
render() {
// 返回虚拟 DOM
return {
type: 'div',
children: `我是文本内容`
}
}
}
有了基本的组件结构之后,渲染器就可以完成组件的渲染,如下面的代码所示:
js
// 用来描述组件的 VNode 对象,type 属性值为组件的选项对象
const CompVNode = {
type: MyComponent
}
// 调用渲染器来渲染组件
renderer.render(CompVNode, document.querySelector('#app'))
渲染器中真正完成组件渲染任务的是 mountComponent 函数,其主要代码逻辑如下:
-
通过虚拟节点(vnode) 获得组件的选项对象,即 vnode.type
-
通过组件的选项对象获得渲染函数 render
-
执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
-
最后调用 patch 函数来挂载组件所描述的内容,即 subTree
js
function mountComponent(vnode, container, anchor) {
// 通过 vnode 获取组件的选项对象,即 vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数 render
const { render } = componentOptions
// 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
const subTree = render()
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
}
真实的 Vue3 源码中的 mountComponent 函数实现会复杂很多,但是原理不变。
这样,就实现了最基本的组件化方案。
总结
当页面很复杂的时候,组件化可以将页面分成很多个部分,不同的部分就是不同的组件,这样便于维护。对于 Vue.js 的使用者来说,组件其实就是对象,而在 Vue.js 渲染器内部,组件是特殊类型的虚拟节点。
每个组件都有 render 函数,渲染器通过调用 render 函数获得该组件需要渲染的视图。
回到一开始的问题,Vue3 是如何渲染组件的?
渲染器根据虚拟节点的 type 属性判断是否为组件,如果是组件,渲染器会使用 mountComponent 和 patchComponent 来完成组件的挂载和更新。
参考
《Vue.js 设计与实现》霍春阳·著