在vue.js 3设计与实现 – 渲染器的设计这章中,我们主要介绍了渲染器的基本概念和整体架构。本章,我们将讲解渲染器的核心功能:挂载与更新。
一、挂载子节点和元素的属性
vue.js 3设计与实现 – 渲染器的设计这章提到,当 vnode.children 的值是字符串类型时,会把它设置为元素的文本内容。一个元素除了具有文本子节点外,还可以包含其他元素子节点,并且子节点可以是很多个。为了描述元素的子节点,我们需要将 vnode.children 定义为数组:
const vnode = {
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
上面这段代码描述的是“一个 div 标签具有一个子节点,且子节点是 p 标签”。可以看到, vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟 DOM 树。 为了完成子节点的渲染,我们需要修改 mountElement 函数,如下面的代码所示:
function mountElement(vnode, container) {
const el = createElement(vnode.type)
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它们
vnode.children.forEach(child => {
patch(null, child, el)
})
}
insert(el, container)
}
在上面这段代码中,我们增加了新的判断分支。使用 Array.isArray 函数判断 vnode.children 是否是数组,如果是数组,则循环遍历它,并调 patch 函数挂载数组中的虚拟节点。在挂载子节点时,需要注意以下两点。
-
传递给 patch 函数的第一个参数是 null。因为是挂载阶段,没有旧 vnode,所以只需要传递 null 即可。这样,当 patch 函数执行时,就会递归地调用 mountElement 函数完成挂载。
-
传递给 patch 函数的第三个参数是挂载点。由于我们正在挂载的子元素是 div 标签的子节点,所以需要把刚刚创建的 div 元素作为挂载点,这样才能保证这些子节点挂载到正确位置。
完成了子节点的挂载后,我们再来看看如何用 vnode 描述一个标签的属性,以及如何渲染这些属性。我们知道, HTML 标签有很多属性,其中有些属性是通用的,例如 id、class 等,而有些属性是特定元素才有的,例如 form 元素的 action 属性。实际上,渲染一个元素的属性比想象中要复杂,不过我们仍然秉承一切从简的原则,先来看看最基本的属性处理。
为了描述元素的属性,我们需要为虚拟 DOM 定义新的 vnode.props 字段,如下面的代码所示:
const vnode = {
type: 'div',
// 使用 props 描述一个元素的属性
props: {
id: 'foo'
},
children: [
{
type: 'p',
children: 'hello'
}
]
}
vnode.props 是一个对象,它的键代表元素的属性名称,它的值代表对应属性的值。这样, 我们就可以通过遍历 props 对象的方式,把这些属性渲染到对应的元素上,如下面的代码所示:
function mountElement(vnode, container) {
const el = createElement(vnode.type)
// 省略 children 的处理
// 如果 vnode.props 存在才处理它
if (vnode.props) {
// 遍历 vnode.props
for (const key in vnode.props) {
// 调用 setAttribute 将属性设置到元素上
el.setAttribute(key, vnode.props[key])
}
}
insert(el, container)
}
在这段代码中,我们首先检查了 vnode.props 字段是否存在,如果存在则遍历它,并调用 setAttribute 函数将属性设置到元素上。实际上,除了使用 setAttribute 函数为元素