虚拟DOM的三个组成部分
虚拟DOM的结构、diff算法和patch过程
问题
虚拟DOM由哪三个部分组成?各自的作用是什么?
解答
虚拟DOM由三个部分组成:VNode(虚拟节点)、diff 算法、patch(打补丁)。
1. VNode - 虚拟节点
用 JavaScript 对象描述真实 DOM 结构。
// 真实 DOM
// <div id="app" class="container">
// <span>Hello</span>
// </div>
// 对应的 VNode
const vnode = {
tag: 'div',
props: {
id: 'app',
class: 'container'
},
children: [
{
tag: 'span',
props: {},
children: ['Hello']
}
]
}
// 创建 VNode 的函数
function h(tag, props, children) {
return {
tag,
props: props || {},
children: children || []
}
}
// 使用
const vnode2 = h('div', { id: 'app' }, [
h('span', null, ['Hello'])
])
2. diff 算法
比较新旧 VNode 的差异,找出需要更新的部分。
// 简化版 diff
function diff(oldVNode, newVNode) {
const patches = []
// 节点类型不同,直接替换
if (oldVNode.tag !== newVNode.tag) {
patches.push({ type: 'REPLACE', newVNode })
return patches
}
// 比较属性
const propsPatches = diffProps(oldVNode.props, newVNode.props)
if (Object.keys(propsPatches).length > 0) {
patches.push({ type: 'PROPS', props: propsPatches })
}
// 比较子节点
const childrenPatches = diffChildren(oldVNode.children, newVNode.children)
patches.push(...childrenPatches)
return patches
}
function diffProps(oldProps, newProps) {
const patches = {}
// 找出修改和新增的属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches[key] = newProps[key]
}
}
// 找出删除的属性
for (const key in oldProps) {
if (!(key in newProps)) {
patches[key] = null
}
}
return patches
}
function diffChildren(oldChildren, newChildren) {
const patches = []
const len = Math.max(oldChildren.length, newChildren.length)
for (let i = 0; i < len; i++) {
if (!oldChildren[i]) {
patches.push({ type: 'ADD', index: i, vnode: newChildren[i] })
} else if (!newChildren[i]) {
patches.push({ type: 'REMOVE', index: i })
} else {
patches.push(...diff(oldChildren[i], newChildren[i]))
}
}
return patches
}
3. patch - 打补丁
将 diff 产生的差异应用到真实 DOM 上。
// 根据 VNode 创建真实 DOM
function createElement(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode)
}
const el = document.createElement(vnode.tag)
// 设置属性
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key])
}
// 递归创建子节点
vnode.children.forEach(child => {
el.appendChild(createElement(child))
})
return el
}
// 应用补丁
function patch(el, patches) {
patches.forEach(p => {
switch (p.type) {
case 'REPLACE':
const newEl = createElement(p.newVNode)
el.parentNode.replaceChild(newEl, el)
break
case 'PROPS':
for (const key in p.props) {
if (p.props[key] === null) {
el.removeAttribute(key)
} else {
el.setAttribute(key, p.props[key])
}
}
break
case 'ADD':
el.appendChild(createElement(p.vnode))
break
case 'REMOVE':
el.removeChild(el.childNodes[p.index])
break
}
})
}
完整示例
// 初始渲染
const oldVNode = h('div', { id: 'app' }, [
h('p', { class: 'text' }, ['Hello'])
])
const container = document.getElementById('root')
const el = createElement(oldVNode)
container.appendChild(el)
// 更新
const newVNode = h('div', { id: 'app' }, [
h('p', { class: 'text-bold' }, ['Hello World'])
])
const patches = diff(oldVNode, newVNode)
patch(el, patches)
关键点
- VNode:用 JS 对象描述 DOM,包含 tag、props、children 三个属性
- diff:同层比较,找出节点的增删改,时间复杂度 O(n)
- patch:根据 diff 结果操作真实 DOM,最小化 DOM 操作
- 优势:减少直接 DOM 操作,批量更新,跨平台渲染
- key 的作用:在列表 diff 时标识节点,提高复用效率
目录