自定义渲染器是前端框架实现跨平台能力的核心,它的本质是将渲染逻辑从具体平台中抽离出来,通过统一的抽象层对接不同的渲染目标。JS实现自定义渲染器的核心在于做好渲染的抽象,让同一套逻辑可以适配浏览器DOM、小程序、Native等不同环境。

什么是渲染的抽象
渲染的抽象指的是把渲染过程中与平台强相关的操作封装成统一的接口,上层渲染逻辑只调用这些接口,不直接操作平台特有的API。比如浏览器中创建元素是document.createElement,而小程序中可能是wx.createView,抽象层会把这两个操作统一成createElement接口,上层逻辑只需要调用createElement即可,不需要关心底层是哪个平台。
渲染抽象通常包含三个核心部分:
- 节点操作抽象:封装创建、插入、删除、更新节点的方法
- 属性操作抽象:封装设置、更新、移除节点属性的方法
- 事件操作抽象:封装事件绑定、解绑的方法
虚拟节点设计
虚拟节点(VNode)是渲染抽象的数据载体,它是对真实节点的JS对象描述,不依赖任何平台。一个基础的虚拟节点结构如下:
// 虚拟节点构造函数
function createVNode(type, props, children) {
return {
type, // 节点类型,比如浏览器中是div、span,自定义平台可以是view、text
props, // 节点属性,包含普通属性和事件
children, // 子节点,可以是字符串或者虚拟节点数组
el // 对应的真实节点引用,渲染后赋值
}
}
实现渲染抽象层
首先我们需要定义渲染抽象层的接口,不同平台只需要实现这些接口即可对接渲染器。这里我们以浏览器和自定义控制台渲染为例,先定义抽象接口:
// 渲染抽象层接口定义
const rendererOptions = {
// 创建元素
createElement(type) {
throw new Error('createElement需要实现')
},
// 插入节点
insert(child, parent, anchor) {
throw new Error('insert需要实现')
},
// 设置属性
patchProp(el, key, prevValue, nextValue) {
throw new Error('patchProp需要实现')
},
// 删除节点
remove(el) {
throw new Error('remove需要实现')
},
// 创建文本节点
createText(text) {
throw new Error('createText需要实现')
},
// 设置文本节点内容
setText(el, text) {
throw new Error('setText需要实现')
}
}
浏览器平台的抽象实现
针对浏览器DOM环境,实现上述抽象接口:
// 浏览器环境的渲染抽象实现
const browserOptions = {
createElement(type) {
return document.createElement(type)
},
insert(child, parent, anchor) {
// anchor为null时插入到末尾
parent.insertBefore(child, anchor || null)
},
patchProp(el, key, prevValue, nextValue) {
if (key.startsWith('on')) {
// 事件处理
const eventName = key.slice(2).toLowerCase()
if (prevValue) {
el.removeEventListener(eventName, prevValue)
}
if (nextValue) {
el.addEventListener(eventName, nextValue)
}
} else {
// 普通属性处理
if (nextValue == null) {
el.removeAttribute(key)
} else {
el.setAttribute(key, nextValue)
}
}
},
remove(el) {
const parent = el.parentNode
if (parent) {
parent.removeChild(el)
}
},
createText(text) {
return document.createTextNode(text)
},
setText(el, text) {
el.nodeValue = text
}
}
实现核心渲染器逻辑
有了抽象层之后,我们就可以实现不依赖具体平台的渲染器核心逻辑,包括挂载和更新两个主要流程。
渲染器挂载逻辑
挂载指的是将虚拟节点转换成真实节点并插入到容器中:
// 渲染器核心函数
function createRenderer(options) {
const {
createElement,
insert,
patchProp,
remove,
createText,
setText
} = options
// 处理子节点
function mountChildren(children, container, anchor) {
if (typeof children === 'string') {
// 文本子节点
const textNode = createText(children)
insert(textNode, container, anchor)
} else if (Array.isArray(children)) {
// 数组子节点,递归挂载
children.forEach(child => {
patch(null, child, container, anchor)
})
}
}
// 挂载元素节点
function mountElement(vnode, container, anchor) {
// 创建真实元素
const el = createElement(vnode.type)
vnode.el = el
// 处理属性
if (vnode.props) {
for (const key in vnode.props) {
patchProp(el, key, null, vnode.props[key])
}
}
// 处理子节点
if (vnode.children) {
mountChildren(vnode.children, el, null)
}
// 插入到容器
insert(el, container, anchor)
}
// 核心patch函数,负责对比新旧节点,执行挂载或更新
function patch(n1, n2, container, anchor) {
if (!n1) {
// 没有旧节点,执行挂载
mountElement(n2, container, anchor)
} else {
// 有旧节点,执行更新逻辑,这里简化只处理同类型节点更新
if (n1.type !== n2.type) {
// 类型不同,先删除旧节点再挂载新节点
remove(n1.el)
mountElement(n2, container, anchor)
} else {
// 类型相同,更新属性和子节点
const el = n2.el = n1.el
// 更新属性
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProp(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
if (!(key in newProps)) {
patchProp(el, key, oldProps[key], null)
}
}
// 更新子节点,简化逻辑只处理文本子节点更新
if (typeof n2.children === 'string') {
if (typeof n1.children === 'string') {
if (n1.children !== n2.children) {
setText(el, n2.children)
}
} else {
// 旧子节点是数组,清空后设置文本
el.innerHTML = ''
const textNode = createText(n2.children)
insert(textNode, el, null)
}
}
}
}
}
// 对外暴露的render函数
function render(vnode, container) {
if (vnode) {
// 有新节点,执行patch
patch(container._vnode, vnode, container, null)
container._vnode = vnode
} else {
// 没有新节点,清空容器
if (container._vnode) {
remove(container._vnode.el)
container._vnode = null
}
}
}
return {
render
}
}
使用自定义渲染器
基于浏览器抽象实现,我们可以创建一个浏览器渲染器并使用它:
// 创建浏览器渲染器
const browserRenderer = createRenderer(browserOptions)
// 创建虚拟节点
const vnode = createVNode('div', {
class: 'container',
onClick: () => console.log('容器被点击')
}, [
createVNode('h1', null, '自定义渲染器示例'),
createVNode('p', null, '这是通过自定义渲染器渲染的内容')
])
// 挂载到页面容器
const container = document.getElementById('app')
browserRenderer.render(vnode, container)
扩展其他平台的渲染器
如果需要适配其他平台,比如自定义的控制台渲染,只需要实现对应的抽象层接口即可,不需要修改渲染器核心逻辑。以下是一个简单的控制台渲染抽象实现:
// 控制台环境的渲染抽象实现
const consoleOptions = {
createElement(type) {
return { type, children: [], props: {} }
},
insert(child, parent) {
parent.children.push(child)
},
patchProp(el, key, prevValue, nextValue) {
el.props[key] = nextValue
},
remove(el) {
// 控制台环境简化删除逻辑
console.log('删除节点', el.type)
},
createText(text) {
return { type: 'text', content: text }
},
setText(el, text) {
el.content = text
}
}
// 创建控制台渲染器
const consoleRenderer = createRenderer(consoleOptions)
// 创建虚拟节点
const consoleVNode = createVNode('view', { class: 'page' }, [
createVNode('text', null, '控制台渲染内容'),
createVNode('text', null, '第二个文本节点')
])
// 自定义容器对象
const consoleContainer = { _vnode: null, children: [] }
// 执行渲染
consoleRenderer.render(consoleVNode, consoleContainer)
console.log('控制台渲染结果', consoleContainer.children)
总结
JS实现自定义渲染器的核心是做好渲染的抽象,通过封装平台相关的操作接口,让渲染核心逻辑可以跨平台复用。整个流程分为四步:设计统一的虚拟节点结构、定义渲染抽象接口、实现不同平台的抽象接口、编写不依赖平台的渲染核心逻辑。这种方式也是React、Vue等框架实现多端渲染的基础思路,理解这个过程可以帮助开发者更深入掌握前端框架的底层工作原理。
JavaScript自定义渲染器渲染抽象虚拟DOM渲染器原理修改时间:2026-07-19 12:57:37