挂载的基本实现
约 1592 字大约 5 分钟
2026-05-15
mountElement 函数的基本设计
在前面的实现中,render 已经将首次渲染和更新渲染统一交给了 patch 处理。而当 patch 发现旧的 vnode 不存在时,就说明当前是首次渲染,此时需要执行挂载操作
所谓挂载,本质上就是把虚拟节点 vnode 转换为真实 DOM,并插入到指定的容器中
mountElement 函数至少需要接收两个参数:
vnode:当前需要被挂载的虚拟节点container:真实 DOM 的挂载容器
实现思路
- 根据
vnode.type创建真实 DOM 元素 - 处理
vnode.children,将子节点挂载到当前元素中 - 将创建好的真实 DOM 元素插入到容器中
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 如果子节点是字符串,则表明他是文本节点
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children);
}
else if (Array.isArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el);
})
}
insert(el, container);
}const vnode1 = {
type: "h1",
children: "hello world",
};
const vnode2 = {
type: "h1",
children: [
{
type: 'p',
children: 'Hello World',
}
],
};笔记
其中,子节点的处理需要区分不同情况:
- 如果
children是字符串,说明它是文本子节点,直接设置元素的文本内容即可 - 如果
children是数组,说明它包含多个子节点,需要遍历数组,并对每个子节点继续调用patch完成挂载
挂载元素属性
子节点挂载完成以后,下一步就需要处理元素本身的属性了,在 vnode 中,可以通过 props 字段来描述元素上的属性:
const vnode = {
type: "h1",
props: {
id: "foo",
}
};为什么属性处理会比较复杂?
因为 HTML attributes 和 DOM properties 并不是完全一一对应的关系,常见情况包括:
- 有的名称不同,例如 HTML 中的
class,对应到 DOM Properties 中是className - 有的只适合作为 HTML attributes 设置,例如
aria-* - 有的只存在于 DOM properties 中,例如
textContent - 有的在 HTML attributes 和 DOM properties 中表现不同,例如表单元素的
value
除了这些情况外,布尔属性也需要特殊注意,例如 disabled:
// 这三种写法都会让按钮处于禁用状态
el.setAttribute("disabled", "");
el.setAttribute("disabled", "false");
el.setAttribute("disabled", false);
// 而使用 DOM Properties 的方式时,只有 true 才表示禁用
el.disabled = true;
el.disabled = false;
el.disabled = "";虽然属性处理存在很多细节,但可以先把握一个基本原则:
如果某个属性在元素对象上存在,就优先使用 DOM Properties 的方式设置;否则再使用 setAttribute。这样做能够尽量让 vnode 中描述的值和真实 DOM 的行为保持一致
实现思路
在挂载元素属性时,遍历 vnode.props:
- 如果当前
key存在于元素对象上,则优先按 DOM Properties 的方式设置 - 如果当前
key不存在于元素对象上,则退回使用setAttribute - 对于布尔类型的 DOM Properties,如果传入的是空字符串,则将其修正为
true
function mountElement(vnode, container) {
const el = createElement(vnode.type);
if (typeof vnode.children === "string") {
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach((child) => {
patch(null, child, el);
});
}
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
// 如果 key 在元素对象上存在,则优先使用 DOM Properties 的方式去设置
if (key in el) {
const type = typeof el[key];
// 对于布尔属性,如果值是空字符串,也应该把它设置为 true
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
el.setAttribute(key, value);
}
}
}
insert(el, container);
}只读 DOM 属性的处理
即使加上了 key in el 这层判断,也仍然会遇到例外情况
例外情况
例如 form 属性:
<form id="form1"></form>
<input form="form1" />对于 input 元素来说,form 属性确实存在于元素对象上,因此 key in el 的结果为 true。但是 el.form 是一个只读属性,它返回的是当前输入框关联的表单元素,不能通过赋值的方式修改
也就是说,下面这种写法并不适合:
el.form = "form1";这种情况下,应该退回到 HTML attributes 的方式进行设置:
el.setAttribute("form", "form1");因此,仅仅通过 key in el 来判断是否应该使用 DOM Properties 并不完全可靠。更合理的方式是,把 "某个属性是否应该作为 DOM Properties 设置" 单独封装成一个函数,在函数内部处理这些特殊情况
实现思路
- 如果是
input元素上的form属性,则返回false,表示应该使用setAttribute - 其他情况下,仍然通过
key in el判断是否优先使用 DOM Properties
function shouldSetAsProps(el, key, value) {
if (key === "form" && el.tagName === "INPUT") {
return false;
}
return key in el;
}function mountElement(vnode, container) {
const el = createElement(vnode.type);
// ...
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (shouldSetAsProps(el, key, value)) {
const type = typeof el[key];
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
el.setAttribute(key, value);
}
}
}
insert(el, container);
}class 处理
class 也属于 props 的一种,但它又比较特殊。在实际使用 Vue 时,class 不仅可以写成字符串,还可以写成数组或对象:
<button class="btn btn-primary"></button>
<button :class="{ btn: true, 'btn-primary': true }"></button>
<button :class="[ 'btn', 'btn-primary' ]"></button>const vnode1 = {
type: "button",
props: {
class: "btn btn-primary",
},
};
const vnode2 = {
type: "button",
props: {
class: {
btn: true,
"btn-primary": true,
},
},
};
const vnode3 = {
type: "button",
props: {
class: ["btn", "btn-primary"],
},
};这就意味着,在真正把 class 设置到 DOM 元素之前,需要先对它进行一次标准化处理。所谓标准化,就是把不同数据结构的 class,统一转换成字符串形式
实现思路
- 如果 class 是字符串,直接使用即可
- 如果 class 是数组,则遍历数组,并递归处理数组中的每一项,最后用空格拼接
- 如果 class 是对象,则遍历对象,把值为 true 的 key 拼接成字符串
import { isString, isArray, isObject } from "@vue/shared";
function normalizeClass(value) {
let res = '';
if (isString(value)) {
res = value;
}
// 数组类型的判断要在对象类型的判断之前,因为数组也是对象
else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i]);
if (normalized) {
res += normalized + ' ';
}
}
}
else if (isObject(value)) {
for (const name in value) {
if (value[name]) {
res += name + ' ';
}
}
}
return res.trim();
}