05.渲染器核心功能
约 2074 字大约 7 分钟
2026-03-01
渲染器的核心功能,是根据拿到的 vnode,进行节点的挂载与更新。
挂载属性
在渲染器内部,会根据 vnode 的 type 类型创建对应的 DOM 节点,然后遍历 vnode.props,将属性挂载到 DOM 节点上。
假设有如下 vnode,其中 props 对应要挂载的属性。挂载属性可以通过遍历 props 并调用 setAttribute 来设置。
const vnode = {
type: 'div',
props: { id: 'foo' },
children: [
{ type: 'p', children: 'hello' },
],
};function mountElement(vnode, container) {
const el = document.createElement(vnode.type);
// 省略对 children 的处理
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key]);
// 还可以使用 DOM 对象方式设置
// el[key] = vnode.props[key];
}
insert(el, container);
}注
这两种实现方式的区别在于:setAttribute 设置的是 HTML Attributes,而 DOM 对象赋值设置的是 DOM Properties。
HTML Attributes
Attributes 是元素的初始属性值,在 HTML 标签中定义,用于描述元素的初始状态
- 在元素被解析时只会初始化一次
- 只能是字符串值,而且这个值仅代表初始状态,无法反映运行时变化
<input type="text" id="username" value="John" />DOM Properties
Properties 是 JavaScript 对象上的属性,代表 DOM 元素在 内存中 的实际状态
- 反映的是 DOM 元素的当前状态
- 属性类型可以是字符串、数字、布尔值、对象等
很多 HTML attributes 在 DOM 对象上有与之相同的 DOM properties,例如:
| HTML attributes | DOM properties |
|---|---|
id="username" | el.id |
type="text" | el.type |
value="John" | el.value |
但是,两者并不总是相等,例如:
| HTML attributes | DOM properties |
|---|---|
class="foo" | el.className |
还有很多其他情况:
- HTML attributes 有但 DOM properties 没有:例如
aria-* - DOM properties 有但 HTML attributes 没有:例如
el.textContent - 一个 HTML attribute 关联多个 DOM properties:例如
value="xxx"与el.value、el.defaultValue都有关联
另外,在设置时不能只用一种方式,而是两种方式结合使用。因为需要考虑很多特殊情况:
disabled- 只读属性
disabled
模板:我们想要渲染的按钮是非禁用状态。
通过 el.setAttribute 设置会遇到的问题:最终渲染出来的按钮仍是禁用状态。
<button :disabled="false">Button</button>const vnode = {
type: 'button',
props: {
disabled: false,
},
};el.setAttribute('disabled', 'false');解决方案:优先设置 DOM properties。
但又会遇到新的问题:本意是要禁用按钮。
在设置 DOM 的 disabled 属性时,任何非布尔类型的值都会被转换为布尔类型。
<button disabled>Button</button>const vnode = {
type: 'button',
props: {
disabled: '',
},
};el.disabled = '';el.disabled = false;最终渲染出来的按钮是非禁用状态。
渲染器内部实现并不是单独用 HTML Attribute 或 DOM Properties,而是两者结合使用,并且会针对特殊情况做特殊处理。
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 用 in 操作符判断 key 是否存在对应的 DOM Properties
if (key in el) {
// 获取该 DOM Properties 的类型
const type = typeof el[key];
const value = vnode.props[key];
// 如果是布尔类型,且 value 是空字符串,则将值矫正为 true
if (type === 'boolean' && value === '') {
el[key] = true;
} else {
el[key] = value;
}
} else {
// 如果没有对应的 DOM Properties,则使用 setAttribute
el.setAttribute(key, vnode.props[key]);
}
}
}
insert(el, container);
}只读属性
例如 el.form 是只读属性,这种情况只能使用 setAttribute。
<input form="form1" />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);
// 省略 children 的处理
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);
}shouldSetAsProps 这个方法返回一个布尔值,由该值决定是否使用 DOM Properties 设置。
还可以进一步优化,将属性设置提取出来:
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === 'form' && el.tagName === 'INPUT') return false;
// 兜底
return key in el;
}
/**
* @param {*} el 元素
* @param {*} key 属性
* @param {*} prevValue 旧值
* @param {*} nextValue 新值
*/
function patchProps(el, key, prevValue, nextValue) {
if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === 'boolean' && nextValue === '') {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 即可
patchProps(el, key, null, vnode.props[key]);
}
}
insert(el, container);
}class 处理
class 本质上也是属性的一种,但 Vue 对其做了增强,因此模板中的 class 可能有以下几种形式。
情况一:字符串值
<template>
<p class="foo bar"></p>
</template>const vnode = {
type: 'p',
props: {
class: 'foo bar',
},
};情况二:对象值
<template>
<p :class="cls"></p>
</template>
<script setup>
import { ref } from 'vue';
const cls = ref({
foo: true,
bar: false,
});
</script>const vnode = {
type: 'p',
props: {
class: { foo: true, bar: false },
},
};情况三:数组值
<template>
<p :class="arr"></p>
</template>
<script setup>
import { ref } from 'vue';
const arr = ref([
'foo bar',
{
baz: true,
},
]);
</script>const vnode = {
type: 'p',
props: {
class: ['foo bar', { baz: true }],
},
};这里第一步是做参数归一化,统一成字符串类型。Vue 内部有 normalizeClass 用于处理此问题。
function isString(value) {
return typeof value === 'string';
}
function isArray(value) {
return Array.isArray(value);
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
function normalizeClass(value) {
let res = '';
if (isString(value)) {
res = value;
} else if (isArray(value)) {
// 如果是数组,递归调用 normalizeClass
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i]);
if (normalized) {
res += (res ? ' ' : '') + normalized;
}
}
} else if (isObject(value)) {
// 如果是对象,则检查每个 key 是否为真值
for (const name in value) {
if (value[name]) {
res += (res ? ' ' : '') + name;
}
}
}
return res;
}
console.log(normalizeClass('foo')); // 'foo'
console.log(normalizeClass(['foo', 'bar'])); // 'foo bar'
console.log(normalizeClass({ foo: true, bar: false })); // 'foo'
console.log(normalizeClass(['foo', { bar: true }])); // 'foo bar'
console.log(normalizeClass(['foo', ['bar', 'baz']])); // 'foo bar baz'const vnode = {
type: 'p',
props: {
class: normalizeClass(['foo bar', { baz: true }]),
},
};const vnode = {
type: 'p',
props: {
class: 'foo bar baz',
},
};设置 class 时也有多种方式:
setAttributeel.className(效率最高)el.classList
function patchProps(el, key, prevValue, nextValue) {
// 对 class 进行特殊处理
if (key === 'class') {
el.className = nextValue || '';
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === 'boolean' && nextValue === '') {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}子节点的挂载
除了处理自身节点,还要处理子节点;处理子节点时会涉及 diff。
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 针对子节点进行处理
if (typeof vnode.children === 'string') {
// 如果 children 是字符串,则直接将字符串插入到元素中
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 挂载
vnode.children.forEach((child) => {
patch(null, child, el);
});
}
insert(el, container);
}面试题:说一说渲染器的核心功能是什么?
参考答案:
渲染器最核心的功能是处理从虚拟 DOM 到真实 DOM 的渲染过程,这个过程包含几个阶段:
- 挂载:初次渲染时,渲染器会将虚拟 DOM 转化为真实 DOM 并插入页面。它会根据虚拟节点树递归创建 DOM 元素并设置相关属性。
- 更新:当组件状态或属性变化时,渲染器会计算新旧虚拟 DOM 的差异,并通过 patch 过程最小化更新真实 DOM。
- 卸载:当组件被销毁时,渲染器需要将其从 DOM 中移除,并进行必要的清理工作。
每一个步骤都有大量细节。就拿挂载来说,光是属性挂载就有很多问题:
- 最终设置属性时,是用
setAttribute,还是给 DOM 对象属性赋值。- 遇到像
disabled这样的特殊属性该如何处理。class、style这样的多值类型,该如何做参数归一化、归一为哪种形式。- 像
class这样的属性,有哪些设置方式,哪一种效率更高。另外,渲染器和响应式系统是紧密结合在一起的。当组件首次渲染时,组件内的响应式数据会和渲染函数建立依赖关系;当响应式数据变化后,渲染函数会重新执行,生成新的虚拟 DOM 树,渲染器随即进入更新阶段,根据新旧两棵虚拟 DOM 树进行最小化更新。这就涉及 Vue 的 diff 算法:Vue 2 采用双端 diff,Vue 3 在此基础上做了进一步优化,采用快速 diff。
