渲染器是 Vue 与浏览器之间的「翻译官」。它拿到一份用 JavaScript 对象描述的 UI(虚拟 DOM),然后精准地创建、更新、销毁真实 DOM,同时把响应式数据和渲染函数绑定成一条自动刷新的流水线。
一、核心职责:挂载 + 更新 + 卸载
渲染器的使命只有三句话:
- 首次出现时把虚拟节点挂载成真实节点;
- 数据变化时用最小代价更新节点;
- 节点消失时把 DOM 和副作用清理干净。
所有细节都围绕这三件事展开。
二、从代码看挂载全流程
js
function mountElement(vnode, container) {
// 1. 创建元素
const el = document.createElement(vnode.type);
// 2. 处理属性
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (key.startsWith('on')) {
// 事件
el.addEventListener(key.slice(2).toLowerCase(), value);
} else if (key === 'class') {
// class 归一化后一次性赋值
el.className = normalizeClass(value);
} else {
// 普通属性
el[key] = value;
}
}
}
// 3. 处理子节点
if (typeof vnode.children === 'string') {
el.textContent = vnode.children;
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => mountElement(child, el));
}
// 4. 插入文档
container.appendChild(el);
}
示例代码已经覆盖「元素创建、属性绑定、事件监听、子节点递归、DOM 插入」五大动作。真实 Vue 只是在此基础上加了 patchFlag、Block Tree 等优化,逻辑完全一致。
三、元素挂载处理细节问题
1. 属性到底用 setAttribute 还是 el[key]?
普通字符串属性推荐 el[key]
,少一次字符串解析,性能更优。布尔属性如 disabled
必须 el.disabled = false
,否则 setAttribute('disabled', 'false')
会把按钮禁用。只读属性如 form
只能 setAttribute
,因为 el.form
是只读。
Vue 内部用 shouldSetAsProps
函数做决策:
js
function shouldSetAsProps(el, key) {
if (key === 'form' && el.tagName === 'INPUT') return false;
return key in el;
}
先判断是否有对应 DOM 属性,再决定走哪条路,确保正确性与性能兼得。
2.class 的特殊处理:字符串、对象、数组一网打尽
模板中的 :class
可能是字符串、对象或数组,渲染器先用 normalizeClass
统一成空格分隔的字符串,再一次性赋给 el.className
,避免多次 DOM 操作。
js
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;
}
normalizeClass(['foo', { bar: true, baz: false }]) // → 'foo bar'
3.子节点的挂载
子节点可能是文本、数组或自定义组件。
- 文本直接
textContent
; - 数组递归
mountElement
; - 组件则执行
mountComponent
,组件再返回新的虚拟节点,继续递归。
整个页面就是一颗虚拟 DOM 树 深度优先地展开成真实 DOM 树。
js
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);
}