这是我参与更文挑战的第6天,活动详情查看: 更文挑战
标题灵感来源评论区:
一、什么是虚拟 DOM
什么是虚拟 DOM?简单来说,虚拟 DOM 就是一个模拟真实 DOM 的树形结构,这个树结构包含了整个 DOM 结构的信息。
正常我们看到的真实 DOM 是这样的:
而虚拟 DOM 则是这样的,包含了标签名称、标签属性、子节点等真实 DOM 信息:
二、为什么使用虚拟 DOM
虚拟 DOM 既然是模拟真实 DOM 的树形结构,那么为什么要用虚拟 DOM 呢?直接操作 DOM 有什么缺点吗?
直接操作 DOM 没有缺点,但是频繁的操作 DOM 就缺点很大,因为操作 DOM 会引起重排,频繁操作 DOM 时,浏览器会频繁重排,导致页面卡顿。
浏览器渲染的大致流程如下:
- 解析 HTML 文档,构建 DOM 树;
- 解析 CSS 属性,构建 CSSOM 树;
- 结合 DOM 树和 CSSOM 树,构建 render 树;
- 在 render 树的基础上进行布局, 计算每个节点的几何结构(重排);
- 把每个节点绘制在屏幕上(重绘);
重排(也叫回流、reflow)就是当涉及到 DOM 节点的布局属性发生变化时,就会重新计算该属性,浏览器会重新描绘相应的元素(上述第 4 步)。
DOM Tree 里的每个节点都会有 reflow 方法,一个节点的 reflow 很有可能导致子节点,甚至父点以及同级节点的 reflow。
因此,为了提升性能,我们应该尽量减少 DOM 操作。
1. 减少 DOM 操作
当有一个表格需要做排序功能时,有出生年月、性别等排序方式可选,当选择某排序方式时,表格将按该方式重新排序。
- 真实 DOM:排序操作需要将表格内的所有 DOM 树删除后新建;
- 虚拟 DOM:使用 diff 算法得到需要修改的部分,仅更新需要发生修改的 DOM 节点;
从上可知,虚拟 DOM 通过 diff 算法,帮助我们大量的减少 DOM 操作。
2. 函数式的 UI 编程方式
从另一个角度看,虚拟 DOM 为我们提供了函数式的编程方式,使代码可读性和可维护性更高。
三、虚拟 DOM 的实现原理
注:该章节的虚拟 DOM 实现原理并不是参比 React 源码,而是参比 simple-virtual-dom,可通过该章节简单了解虚拟 DOM 实现原理,React 中的虚拟 DOM 实现可查看 React 官网 Virtual DOM 及内核。
虚拟 DOM 通过以下步骤实现:
- 构建虚拟 DOM 树;
- 比较新旧虚拟 DOM 树差异;
- 更新真实 DOM;
1. 构建虚拟 DOM
模拟真实 DOM 树,构建虚拟 DOM 树结构,包含标签名 tagName、属性对象 props、子节点 children、子节点数 count 等属性。
function Element (tagName, props = {}, children = []) {
// 标签名
this.tagName = tagName
// 属性对象
this.props = props
// 子节点
this.children = children
// key标志
const { key = void 666 } = this.props
this.key = key
// 子节点数量
let count = 0
this.children.forEach((child, index) => {
if (child instanceof Element) {
count += child.count
}
count++
})
this.count = count
}
创建虚拟 DOM 对象:
console.log(el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom'])
]))
生成的虚拟 DOM 对象如图:
将虚拟 DOM 转换为真实 DOM:
Element.prototype.render = function () {
const el = document.createElement(this.tagName)
const props = this.props
for (const propName in props) {
const propValue = props[propName]
_.setAttr(el, propName, propValue)
}
this.children.forEach((child) => {
let childEl
if (child instanceof Element) {
childEl = child.render()
} else {
childEl = document.createTextNode(child)
}
el.appendChild(childEl)
})
return el
}
填充进页面:
document.body.appendChild(el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom'])
]).render())
效果如图:
2. 比较两棵虚拟 DOM 树的差异
当数据更新时,需要对新旧虚拟 DOM 树进行对比。
- 当新旧节点都是字符串类型时,直接替换;
if (_.isString(oldNode) && _.isString(newNode)) {
if (newNode !== oldNode) {
currentPatch.push({ type: patch.TEXT, content: newNode })
}
// Nodes are the same, diff old node's props and children
}
- 当新旧节点的标签名、key 值相等时,对比属性 Props 以及子节点 children;
if (
oldNode.tagName === newNode.tagName &&
oldNode.key === newNode.key
) {
// Diff props
var propsPatches = diffProps(oldNode, newNode)
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches })
}
// Diff children. If the node has a `ignore` property, do not diff children
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
}
}
- 如果新节点存在,且和旧节点标签名不同,或者 key 不同,则直接将新节点替换为旧节点。
currentPatch.push({
type: PATCH_KEY.REPLACE,
node: newNode
})
总结一下,虚拟 DOM 只在同层级间 Diff,如果标签不同则直接替换该节点及其子节点。
尝试对比虚拟 DOM 如下:
function renderTree () {
return el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['the count is :' + Math.random()])
])
}
let tree = renderTree()
setTimeout(() => {
const newTree = renderTree()
const patches = diff(tree, newTree)
console.log(patches)
}, 2000)
对比差异为 p 标签的文本节点发生改变,输出结果如图:
3. 对真实 DOM 进行最小化修改
最后一步是根据 diff 结果,对真实 DOM 进行修改。
遍历真实 DOM 树,如果该 DOM 节点有 diff,则根据 diff 类型,处理 DOM 节点,如果该 DOM 节点无 diff,则遍历其子节点,直至遍历完成。
注:React 实现更优,具体请见 React fiber。
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index]
var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) {
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
if (currentPatches) {
applyPatches(node, currentPatches)
}
}
尝试更新真实 DOM,代码如下:
function renderTree () {
return el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['the count is :' + Math.random()])
])
}
let tree = renderTree()
const root = tree.render()
document.body.appendChild(root)
setTimeout(() => {
const newTree = renderTree()
const patches = diff(tree, newTree)
patch(root, patches)
tree = newTree
}, 2000)
效果如图:
上图可见,成功更新真实 DOM。
四、总结
本文从什么是虚拟 DOM、为什么使用虚拟 DOM、虚拟 DOM 的实现原理等 3 个角度对虚拟 DOM 进行讲述。
虚拟 DOM 通过模拟真实 DOM 的树结构,收集大量 DOM 操作,通过 diff 算法对真实 DOM 进行最小化修改,减少浏览器重排,提升加载速度,达到优化网站性能的作用。
虚拟 DOM 采用函数式编程,让我们码得更好看更快乐。
可通过 github源码 进行实操练习。
希望能对你有所帮助,感谢阅读~
别忘了点个赞鼓励一下我哦,笔芯❤️