Appearance
渲染器架构:createRenderer 的设计
首先要问的是:Vue 可以渲染到 DOM、Canvas、Native,是如何做到的?
答案是平台无关的渲染器设计——这是一个非常经典的架构模式,在很多跨平台框架中都能看到。
渲染器的职责
渲染器做三件事:
- 挂载:将 VNode 渲染为真实节点
- 更新:对比新旧 VNode,更新真实节点
- 卸载:移除不需要的节点
javascript
function render(vnode, container) {
if (vnode) {
// 有新 vnode,执行 patch
patch(container._vnode, vnode, container)
} else {
// 没有新 vnode,卸载
if (container._vnode) {
unmount(container._vnode)
}
}
// 保存旧 vnode
container._vnode = vnode
}平台无关的设计
问题:渲染器如何支持多平台?
javascript
// DOM
document.createElement('div')
// Canvas
ctx.fillRect(...)
// Native
NativeModule.createView(...)解决方案:将平台相关操作抽象为选项。
javascript
const browserOptions = {
createElement(type) {
return document.createElement(type)
},
insert(child, parent, anchor) {
parent.insertBefore(child, anchor)
},
setElementText(el, text) {
el.textContent = text
},
remove(child) {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
}
// ...
}渲染器内部使用这些函数,而不是直接调用 DOM API。
createRenderer 设计
javascript
function createRenderer(options) {
// 解构平台操作函数
const {
createElement,
insert,
remove,
setElementText,
patchProp,
createText,
setText
} = options
// 渲染器内部函数
function patch(n1, n2, container, anchor) {
// ...
}
function mountElement(vnode, container, anchor) {
const el = vnode.el = createElement(vnode.type)
// ...
insert(el, container, anchor)
}
function unmount(vnode) {
remove(vnode.el)
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode || null, vnode, container)
} else if (container._vnode) {
unmount(container._vnode)
}
container._vnode = vnode
}
return {
render,
createApp: createAppAPI(render)
}
}rendererOptions 完整定义
javascript
const rendererOptions = {
// 创建元素
createElement(type, isSVG, isCustom, props) {
return isSVG
? document.createElementNS('http://www.w3.org/2000/svg', type)
: document.createElement(type, isCustom ? { is: props.is } : undefined)
},
// 创建文本节点
createText(text) {
return document.createTextNode(text)
},
// 创建注释节点
createComment(text) {
return document.createComment(text)
},
// 设置文本内容
setText(node, text) {
node.nodeValue = text
},
// 设置元素文本
setElementText(el, text) {
el.textContent = text
},
// 获取父节点
parentNode(node) {
return node.parentNode
},
// 获取下一个兄弟节点
nextSibling(node) {
return node.nextSibling
},
// 插入节点
insert(child, parent, anchor = null) {
parent.insertBefore(child, anchor)
},
// 移除节点
remove(child) {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// 设置属性
patchProp(el, key, prevValue, nextValue, isSVG) {
// 处理 class、style、事件、普通属性等
}
}patch 函数设计
patch 是渲染器的核心,根据 VNode 类型分发处理:
javascript
function patch(n1, n2, container, anchor = null) {
// n1: 旧 VNode
// n2: 新 VNode
// 类型不同,直接卸载旧的
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container)
break
case Comment:
processComment(n1, n2, container)
break
case Fragment:
processFragment(n1, n2, container)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor)
}
}
}processElement
javascript
function processElement(n1, n2, container, anchor) {
if (n1 === null) {
// 挂载
mountElement(n2, container, anchor)
} else {
// 更新
patchElement(n1, n2)
}
}processText
javascript
function processText(n1, n2, container) {
if (n1 === null) {
// 挂载文本节点
const el = n2.el = createText(n2.children)
insert(el, container)
} else {
// 更新文本内容
const el = n2.el = n1.el
if (n2.children !== n1.children) {
setText(el, n2.children)
}
}
}processFragment
javascript
function processFragment(n1, n2, container) {
if (n1 === null) {
// 挂载 Fragment 的子节点
mountChildren(n2.children, container)
} else {
// 更新子节点
patchChildren(n1, n2, container)
}
}createApp 设计
createApp 是用户创建应用的入口:
javascript
function createAppAPI(render) {
return function createApp(rootComponent, rootProps = null) {
const app = {
_component: rootComponent,
_props: rootProps,
_container: null,
mount(containerOrSelector) {
// 获取容器
const container = typeof containerOrSelector === 'string'
? document.querySelector(containerOrSelector)
: containerOrSelector
// 创建根组件的 VNode
const vnode = createVNode(rootComponent, rootProps)
// 渲染
render(vnode, container)
app._container = container
// 返回组件实例的代理
return vnode.component?.proxy
},
unmount() {
render(null, app._container)
}
}
return app
}
}使用:
javascript
const { createApp } = createRenderer(rendererOptions)
createApp({
setup() {
return () => h('div', 'Hello World')
}
}).mount('#app')默认渲染器
runtime-dom 提供默认的 DOM 渲染器:
javascript
let renderer
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
export const render = (...args) => {
ensureRenderer().render(...args)
}
export const createApp = (...args) => {
return ensureRenderer().createApp(...args)
}自定义渲染器示例
创建一个渲染到 Canvas 的渲染器:
javascript
const canvasRenderer = createRenderer({
createElement(type) {
// 返回一个虚拟节点对象
return { type, props: {}, children: [] }
},
insert(child, parent) {
parent.children.push(child)
},
setElementText(el, text) {
el.text = text
},
patchProp(el, key, prev, next) {
el.props[key] = next
},
remove(child) {
// 从父节点移除
}
})
// 使用
const app = canvasRenderer.createApp(CanvasApp)
app.mount(virtualContainer)
// 最后遍历虚拟树,绘制到 Canvas
function drawToCanvas(tree, ctx) {
// ...
}完整的 createRenderer
javascript
function createRenderer(options) {
const {
createElement: hostCreateElement,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText,
patchProp: hostPatchProp,
createText: hostCreateText,
setText: hostSetText
} = options
function patch(n1, n2, container, anchor = null) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container)
break
case Fragment:
processFragment(n1, n2, container)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor)
}
}
}
function processElement(n1, n2, container, anchor) {
if (n1 === null) {
mountElement(n2, container, anchor)
} else {
patchElement(n1, n2)
}
}
function mountElement(vnode, container, anchor) {
const el = vnode.el = hostCreateElement(vnode.type)
if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children)
} else if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(vnode.children, el)
}
if (vnode.props) {
for (const key in vnode.props) {
hostPatchProp(el, key, null, vnode.props[key])
}
}
hostInsert(el, container, anchor)
}
function unmount(vnode) {
hostRemove(vnode.el)
}
function render(vnode, container) {
if (vnode) {
patch(container._vnode || null, vnode, container)
} else if (container._vnode) {
unmount(container._vnode)
}
container._vnode = vnode
}
return {
render,
createApp: createAppAPI(render)
}
}本章小结
渲染器架构的核心设计:
- createRenderer:接收平台选项,返回渲染器
- rendererOptions:抽象平台操作(createElement、insert 等)
- patch:根据 VNode 类型分发处理
- createApp:创建应用实例的工厂函数
这种设计使得 Vue 可以渲染到任何目标平台。
练习与思考
实现一个简化版的
createRenderer。设计一个渲染到控制台的渲染器(输出 VNode 树的文本表示)。
思考:为什么 Vue 把
runtime-core和runtime-dom分开?