大家好,我是你们的阿达西,一个又菜又爱玩的前端菜鸟。今天开始尝试一些更深入的东西,因为我发现侃侃而谈一些东西比起"额,这个...,没太了解过" 更吸引人,不懂得话那样太窘迫了,尴尬的想钻进地缝里面。本篇文章主要是在深入阅读前辈们的diff理解,进行整理的。
你了解虚拟DOM吗?给我说说vue里面的diff
前面觉得这种问题还是比较深层次的,我应该还要晚点才能接触到,所以没有去了解过,但是后来发现被问的概率太大了,就促使我来搞一搞这个了。
为什么要有虚拟DOM
我们都知道浏览器在渲染的时候要进行一系列的操作:
- 解析HTML生成DOM树
- 解析CSS生成cssOM树
- 合成生成渲染树
- 进行样式计算
- 布局
- 绘制
- 合成
每次修改DOM都会触发这些步骤的一个或多个步骤。我们都知道DOM操作是很耗性能的,因为会触发重绘和回流,以及遍历节点等。所以此时virtual DOM
就"铛铛铛"的产生了。虚拟DOM其实就是真实DOM的抽象,通过js对象来模拟真实的DOM树。 例如:
js
<div class="dd">
<p>123</p>
</div>
虚拟dom就是:
js
var Vnode = {
tag: 'div',
props:{
className:"dd"
},
children: [
{ tag: 'p', text: '123' }
]
};
这样的 DOM 结构就称之为 虚拟 DOM (Virtual DOM
)
它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tag
、props
、children
- tag:必选。就是标签。也可以是组件,或者函数
- props:非必选。就是这个标签上的属性和方法
- children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素
虚拟DOM它可以减少浏览器的重绘回流,只更新需要更新的部分,这就牵扯到了虚拟DOM的diff算法了。
diff算法
diff算法就是找出新旧两棵虚拟DOM树上的不同。但是我们知道两棵树找不同还是很复杂的,引用大佬的话就是
我们知道,两棵树做 diff,复杂度是 O(n^3) 的,因为每个节点都要去和另一棵树的全部节点对比一次,这就是 n 了,如果找到有变化的节点,执行插入、删除、修改也是 n 的复杂度。所有的节点都是这样,再乘以 n,所以是 O(n * n * n) 的复杂度。
作者:zxg_神说要有光
我当时就在想一个可能比较幼稚的问题,为什么不能直接每个同层次的节点进行比较,而是一个节点要比较另一棵树所有的节点,这不纯纯浪费吗?为什么会是这种计算方式呢?有没有大佬能解惑一下。
一个小疑惑,咱们继续回到上面,所以diff算法就有了这样的约定:只进行同层次的比较,相同就继续往下比,不同的话就直接替换掉。但是这里就有一点问题了:如果仅仅只是第一个不同后面的都相同,那还要全部替换,这不是纯纯浪费吗? 例如:
所以,就有了以下的几种diff:
简单diff(多节点diff)
就是让每个节点都有一个唯一的标识(这也就是我们的v-for为什么需要key的原因了)通过key来判断是不是存在这个节点,如果存在的话就只需要移动节点就行了,不再需要重新创造个节点或者移除了。
它的核心就是
- 通过唯一标识判断是否存在这个这个节点,存在就表示可以复用,可能只需要移动即可。不存在就表示需要创建节点。
- 判断是否需要移动,就看该节点是不是在最后一个匹配到旧节点的位置,如果是的话就不需要移动,不是的话就需要对该节点进行移动了。
- 最后就是移除一些没有用到的结点
js
const oldChildren = n1.children
const newChildren = n2.children
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
let j = 0
let find = false
// 遍历旧的 children
for (j; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j]
// 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新
if (newVNode.key === oldVNode.key) {
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1]
if (prevVNode) {
const anchor = prevVNode.el.nextSibling
insert(newVNode.el, container, anchor)
}
} else {
// 更新 lastIndex
lastIndex = j
}
break
}
}
if (!find) {
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
anchor = prevVNode.el.nextSibling
} else {
anchor = container.firstChild
}
patch(null, newVNode, container, anchor)
}
}
// 遍历旧的节点
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
// 拿着旧 VNode 去新 children 中寻找相同的节点
const has = newChildren.find(
vnode => vnode.key === oldVNode.key
)
if (!has) {
// 如果没有找到相同的节点,则移除
unmount(oldVNode)
}
}
根据这个过程,ABCD要变成BCDA的,就要移动三次,这其实也是很费时间的,所以我们必须还要对其进行优化...
优化
本篇文章先着重弄清楚diff算法的一个原理,下篇文章再进行一些优化的学习,以及学习v2,v3,以及react的diff算的不同之处。以上是我通过前辈们对diff算法的理解在进行的一个自我的总结和归纳,感谢大家的观看!!!