7.3 自定义渲染器
把 DOM 操作抽成
options传入createRenderer,核心逻辑就不再依赖浏览器 API,换一套options即可渲染到 canvas / 小程序 / Node 等任意平台。
一、vnode 与挂载
用对象描述一个 vnode,type 描述 vnode 的类型:
const vnode = {
type: 'h1', // 字符串类型 → 普通标签,值即标签名
children: 'hello' // 字符串 → 该元素的文本子节点
}
挂载一个 vnode 的三步:
- 用
vnode.type作为标签名创建元素; - 处理
vnode.children:值是字符串就代表元素具有文本子节点,设置元素的textContent即可; - 把新元素插入容器。
二、第一版:直接依赖浏览器 API
function createRenderer() {
function mountElement(vnode, container) {
const el = document.createElement(vnode.type) // 创建 DOM 元素
if (typeof vnode.children === 'string') {
el.textContent = vnode.children // 字符串子节点 → 文本节点
}
container.appendChild(el) // 添加到容器
}
// n1 旧 vnode,n2 新 vnode
function patch(n1, n2, container) {
if (!n1) {
mountElement(n2, container) // 旧的不存在 → 挂载
} else {
// 打补丁,暂时省略
}
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
container.innerHTML = '' // 卸载
}
}
container._vnode = vnode
}
return { render }
}
const renderer = createRenderer()
renderer.render(
{ type: 'h1', children: 'hello 11' },
document.querySelector('#app')
)
问题:
document.createElement、textContent、appendChild全是 浏览器特有 API,这段渲染器只能在浏览器跑,无法渲染到其他平台。
三、第二版:把平台 API 抽成 options
思路:把操作 DOM 的 API 封装成一个对象传给 createRenderer,用配置项里的 API 重写 mountElement。
function createRenderer(options) {
// 通过 options 得到操作 DOM 的 API(作用域内的函数都能访问它们)
const { createElement, insert, setElementText } = options
function mountElement(vnode, container) {
const el = createElement(vnode.type) // 创建元素
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children) // 设置文本
}
insert(el, container) // 插入容器
}
function patch(n1, n2, container) {
if (!n1) {
mountElement(n2, container)
} else {
// 打补丁,暂时省略
}
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
container.innerHTML = ''
}
}
container._vnode = vnode
}
return { render }
}
浏览器平台的 options:
const renderer = createRenderer({
// 创建元素
createElement(tag) {
return document.createElement(tag)
},
// 设置元素的文本节点
setElementText(el, text) {
el.textContent = text
},
// 在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
}
})
renderer.render({ type: 'h1', children: 'hello 11' }, document.querySelector('#app'))
- 重构后
mountElement功能完全没变,但不再直接依赖浏览器特有 API; - 只要传入不同的
options,就能在非浏览器环境完成渲染工作 —— 这就是自定义渲染器。
易错点:
insert的anchor默认null,insertBefore(el, null)等价于appendChild(el);保留anchor参数是为了后面「把元素插入到指定元素之前」时不用再改接口。
四、第三版:一个非 DOM 平台的自定义渲染器
实现一个打印渲染器操作流程的渲染器:createElement 不调浏览器 API,只返回一个对象 { tag } 当作创建出的「DOM 元素」。
const renderer = createRenderer({
createElement(tag) {
console.log(`创建元素 ${tag}`)
return { tag }
},
setElementText(el, text) {
console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`)
el.textContent = text
},
insert(el, parent, anchor = null) {
console.log(`将 ${JSON.stringify(el)} 添加到 ${JSON.stringify(parent)} 下`)
parent.children = el
}
})
const vnode = { type: 'h1', children: 'hello' }
const container = { type: 'root' } // 用对象模拟挂载点
renderer.render(vnode, container)
// 控制台输出:
// 创建元素 h1
// 设置 {"tag":"h1"} 的文本内容:hello
// 将 {"tag":"h1"} 添加到 {"type":"root"} 下
- 这套
options不依赖浏览器 API,所以浏览器和 Node.js 都能运行; - 同理可对接 canvas、小程序等平台:只需为
createElement/setElementText/insert提供该平台的实现。
五、小结
- 自定义渲染器不是「黑魔法」:只是通过抽象让核心代码不再依赖平台特有 API,再通过支持个性化配置的能力实现跨平台;
- 需要抽象的典型平台 API:
createElement、createText、setElementText、insert(本章只涉及元素相关的最小集合); - Vue 3 的
createRenderer正是这个套路,@vue/runtime-dom提供的renderer就是传入 DOM 版options的产物。
关键词:通用渲染器 = 稳定的核心逻辑(挂载 / 打补丁)+ 可替换的平台实现(options)。
相关
- 7.2 渲染器的基本概念:renderer / vnode / mount / patch 术语与渲染器骨架