手写极简渲染器实现 mount 和 patch:一、定义 vnode(tag/props/children);二、mount 递归创建 DOM;三、patch 对比更新文本、替换元素或复用标签并更新 props 与子节点;四、组合驱动响应式更新。

分析 Vue 3 Composition API 使用情况——审计响应式模式、可组合函数设计、ref 与 reactive 的选择、计算属性、watcher 使用及组件……
<p>我们不依赖 Vue 源码,也不用编译器,只用最基础的 JavaScript,手写一个极简渲染器,实现 <strong>mount</strong>(首次挂载)和 <strong>patch</strong>(更新比对)两个核心能力。</p>
<H3>一、先定义虚拟节点 vnode</H3>
<p>Vue 的响应式更新始于 vnode —— 它是真实 DOM 的轻量级描述。我们只支持最简结构:</p>
<ul>
<li><strong>tag</strong>:字符串,如 <code>"div"</code>、<code>"span"</code></li>
<li><strong>props</strong>:对象,如 <code>{ id: "app", class: "box" }</code></li>
<li><strong>children</strong>:数组,元素为字符串(文本)或其它 vnode</li>
</ul>
<p>构造函数写成一个普通函数即可:</p>
<pre><font size="2">function h(tag, props = {}, children = []) {
return { tag, props, children };
}</font></pre>
<H3>二、实现 mount:把 vnode 渲染成真实 DOM</H3>
<p><strong>mount</strong> 是一次性创建 DOM 并插入容器的过程。我们不递归处理响应式,只做纯渲染:</p>
<ul>
<li>遇到字符串子节点 → 创建 <code>Text</code> 节点</li>
<li>遇到 vnode 子节点 → 递归调用 mount</li>
<li>最后把所有子节点 append 到父元素上</li>
</ul>
<pre><font size="2">function mount(vnode, container) {
const el = document.createElement(vnode.tag);
// 设置属性(简化版:只处理 class 和普通 attr)
for (const key in vnode.props) {
if (key === 'class') {
el.className = vnode.props[key];
} else {
el.setAttribute(key, vnode.props[key]);
}
}
// 挂载子节点
vnode.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else {
mount(child, el); // 递归
}
});
container.appendChild(el);
}</font></pre>
<H3>三、实现 patch:新旧 vnode 对比并更新 DOM</H3>
<p><strong>patch</strong> 的目标是复用已有 DOM,只改需要变的部分。我们只处理三种情况:</p>
<ul>
<li>新旧都是文本 → 更新 textContent</li>
<li>新旧 tag 不同 → 替换整个元素(卸载旧的,mount 新的)</li>
<li>新旧 tag 相同 → 复用元素,更新 props + 递归 patch 子节点</li>
</ul>
<p>注意:这里不实现 diff 算法(如双端比较),只做最朴素的“按序 patch”:</p>
<pre><font size="2">function patch(oldVNode, newVNode) {
const el = newVNode.el = oldVNode.el;
// 文本节点更新
if (typeof oldVNode === 'string' && typeof newVNode === 'string') {
if (oldVNode !== newVNode) {
el.textContent = newVNode;
}
return;
}
// 标签不同,直接替换
if (oldVNode.tag !== newVNode.tag) {
const parent = el.parentNode;
parent.replaceChild(mount(newVNode, document.createElement('div')).firstChild, el);
return;
}
// 标签相同:更新属性
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
// 移除旧有但新中没有的属性
for (const key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 设置/更新新属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
if (key === 'class') {
el.className = newProps[key];
} else {
el.setAttribute(key, newProps[key]);
}
}
}
// patch 子节点:简单按索引一一对应
const oldChildren = oldVNode.children || [];
const newChildren = newVNode.children || [];
const commonLength = Math.min(oldChildren.length, newChildren.length);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 多余旧子节点移除
if (oldChildren.length > commonLength) {
for (let i = commonLength; i < oldChildren.length; i++) {
const child = el.childNodes[i];
if (child) child.remove();
}
}
// 新增子节点挂载
if (newChildren.length > commonLength) {
for (let i = commonLength; i < newChildren.length; i++) {
mount(newChildren[i], el);
}
}
}</font></pre>
<H3>四、组合使用:模拟 Vue 的 render + reactivity</H3>
<p>有了 <code>mount</code> 和 <code>patch</code>,你就能手动驱动更新了。例如:</p>
<pre><font size="2">// 初始 vnode
const initialVNode = h('div', { id: 'app' }, [
h('h1', {}, ['Hello']),
h('p', {}, ['Count: ', '0'])
]);
// 挂载
const app = document.getElementById('app');
mount(initialVNode, app);
// 后续更新(比如点击后 count 变为 1)
const nextVNode = h('div', { id: 'app' }, [
h('h1', {}, ['Hello']),
h('p', {}, ['Count: ', '1'])
]);
patch(initialVNode, nextVNode); // 复用 div,只改 p 里的文本</font></pre>
<p>若想进一步接入响应式系统(如用 Proxy 跟踪 state 变化),只需在数据变更时重新生成 vnode 并调用 <code>patch</code> 即可 —— 这正是 Vue 3 的核心思路。</p>

















