第七章:渲染器的设计
createRenderer ---创建---> 渲染器(renderer) ---调用render---> 挂载 ---调用render---> 更新 ---调用render---> 卸载
createRenderer - 创建渲染器
- render - newVNode container
function render(vNode, container) {
if (vNode) {
patch(container._vNode, vNode, container)
} else {
if (container._vNode) {
// 调用unmount移除
unmount(container._vNode)
}
}
container._vNode = vNode
}
- patch - 比对新旧虚拟DOM,更新真实DOM
function patch (oldVNode, newVNode, container) {
// 如果vNode1不存在,则意味着挂载,调用mountElement完成挂载
if (!oldVNode) {
mountElement(newVNode, container)
} else {
// vNode1存在,意味着打补丁
}
}
- patchProps - 解析虚拟DOM中的props
patchProps(el, key, preValue, nextValue) {
if (key === 'class') {
el.className = nextValue || ''
} else if (shouldSetAsProps(el. key, nextValue)) {
const type = typeof el[key]
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
el.setAttribute(key, nextValue)
}
}
- mountElement - 将虚拟DOM转为真是DOM并挂载到 container 上
function mountElement(vNode, container) {
const el = vNode.el = createElement(vNode.type)
if (typeof vNode.children === 'string') {
setElementText(el, vNode.children)
} else if (Array.isArray(vNode.children)) {
vNode.children.forEach(child => {
patch(null, child, el)
})
}
if (vNode.props) {
for (const key in vNode.props) {
patchProps(el, key, null, vNode.props[key])
}
}
insert(el, container)
}
- unmount - 移除元素/组件
function unmount(vNode) {
// 获取虚拟节点对应真实DOM的父节点
const parent = vNode.el.parentNode
if (parent) {
// 移除虚拟节点对应的真实DOM
parent.removeChild(vNode.el)
}
}
7.1:渲染器与响应系统的结合
const count = ref(1)
effect(() => {
renderer(`<h1>{count.value}</h1>`, document.getElementById('app'))
})
count.value++
副作用函数内调用renderer函数执行渲染。副作用函数执行完毕后,会与响应式数据建立响应联系。当修改
count.value的值时,副作用函数会重新执行,完成重新渲染。
7。2:渲染器基本概念
- 渲染器的作用时把虚拟DOM渲染为特定平台上的真实元素。在浏览器上,渲染器会把虚拟DOM渲染为真实DOM元素。
- 渲染器将虚拟DOM节点渲染为真实DOM节点的过程叫做挂载,这就意味着在
mounted钩子中可以访问真实DOM元素。 - 渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。渲染器会把该DOM元素作为容器,并把内容渲染到其中。
- 渲染器的内容非常广泛,用来把vNode渲染为真实DOM的render函数只是其中一部分。
渲染器会使用newVNode与上一次渲染的oldVNode进行比较,视图找到并更新变更点。这个过程叫做打补丁。 但实际上,挂载动作本身也可以看作一种特殊的打补丁,它的特殊之处在于oldVNode不存在。(不用太纠结挂载/打补丁两个概念)
function createRenderer () {
function render (vNode, container) {
if (vNode) {
// 新vNode存在,将其与vNode一起传递给 patch 函数,进行打补丁
patch(container._vNode, vNode, container)
} else {
if (container._vNode) {
// 旧vNode存在,且新vNode不存在,说明时 卸载 操作
// 只需要将 container 内的DOM清空即可
container.innerHTML = ''
}
}
// 把 vNode 存储到 container._vNode下,即后续渲染中的旧 vNode
container._vNode = vNode
}
return {
render
}
}
结合上面和下面的代码来分析执行流程,从而理解render函数的实现思路。
const renderer = createRenderer()
const app = document.querySelector('#app')
// 第一次渲染
renderer.render(vNode1, app)
// 第二次渲染
renderer.render(vNode2, app)
// 第三次渲染
renderer.render(null, app)
首次渲染时。渲染器会将 vNode1 渲染为真实DOM。渲染完成后,vNode1 会存储到容器元素container._vNode属性中, 它会在后续渲染中作为 oldVNode 使用。
第二次渲染时,oldVNode存在,此时渲染器会把 vNode2 作为新 vNode,并将新旧 vNode 同时传递给patch函数进行打补丁。
第三次渲染时,newVNode 的值为null,即什么都不渲染。但此时容器中渲染的时 vNode2 所描述的内容,所以 渲染器需要清空容器。
7.3:自定义渲染器
渲染器不仅能够把虚拟DOM渲染为浏览器平台上的真实DOM。通过将渲染器设计为可配置的通用渲染器,即可实现 渲染到任意目标平台上。
补充patch
function createRenderer () {
function patch (vNode1, vNode2, container) {
// 如果vNode1不存在,则意味着挂载,调用mountElement完成挂载
if (!vNode1) {
mountElement(vNode2, container)
} else {
// vNode1存在,意味着打补丁
}
}
function mountElement (vNode, container) {
// 创建DOM元素
const el = document.createElement(vNode.type)
// 处理子节点,如果子节点是字符串,代表元素具有文本节点
if (typeof vNode.children === 'string') {
// 因此只需要设置元素的 textContent 属性即可
el.textContent = vNode.children
}
// 将元素添加到容器中
container.appendChild(el)
}
function render (vNode, container) {
if (vNode) {
// 新vNode存在,将其与vNode一起传递给 patch 函数,进行打补丁
patch(container._vNode, vNode, container)
} else {
if (container._vNode) {
// 旧vNode存在,且新vNode不存在,说明时 卸载 操作
// 只需要将 container 内的DOM清空即可
container.innerHTML = ''
}
}
// 把 vNode 存储到 container._vNode下,即后续渲染中的旧 vNode
container._vNode = vNode
}
return {
render
}
}
目标是设计不依赖于浏览器平台的通用渲染器,但mountElement函数内调用了大量依赖于浏览器的API。 例如 document.createElement、el.textContent、appendChild等。想要设计通用渲染器, 第一步要做的就是将这些浏览器特有的API抽离。
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)
}
})
function createRenderer (options) {
// 通过options得到操作DOM的API
const {
createElement,
insert,
setElementText
} = options
// 在这个作用域内定义的函数都可以访问那些API
function mountElement(vNode, container) {
const el = createElement(vNode.type)
if (typeof vNode.children === 'string') {
setElementText(vNode.children)
}
insert(el, container)
}
function patch (vNode1, vNode2, container) {
// ...
}
}
自定义渲染只是通过抽象的手段,让核心代码不再依赖平台特有的API,再通过支持个性化配置的能力来实现跨平台。
为了让渲染器不直接依赖浏览器平台特有的API,我们将这些用来创建、修改和删除元素的操作抽象成可配置的对象。 用户可以再调用
createRenderer函数创建渲染器的时候指定自定义的配置对象,从而实现自定义的行为。