虚拟 DOM 是什么?
DOM 是很慢的。如果把一个简单的 div
元素的所有属性都打印出来,可以看到:
并且这只是第一层,真正的 DOM 元素非常庞大,因为标准就是如此。
相对于 DOM 对象,原生 JavaScript 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息都可以很容易的利用 Javascript 对象表示出来:
const element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
对应的HTML的写法是:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
DOM 树的信息都可以用 Javascript 对象来表示,就可根据这个用 Javascript 对象表示的树结构来构建一棵真正的DOM树。
虚拟DOM是相对真实DOM的一个概念。他实际是把DOM对象抽象起来成一个javascript对象。虚拟DOM是和新一代框架组件概念想对应的。在框架内部可以对虚拟DOM进行diff来达到优化的效果。
虚拟 DOM 的优点
减少 DOM 操作
-
减少 DOM 操作虚拟 DOM 可以将多次操作合并为一次操作,比如你添加 1000 个节点,却是一个接一个操作的(减少频率)
-
虚拟 DOM 借助 DOM diff 可以把多余的操作省掉,比如你添加 1000 个节点,其实只有 10 个是新增的(减少范围)
跨平台
虚拟 DOM 不仅可以变成 DOM,还可以变成小程序、iOS 应用、安卓应用,因为虚拟 DOM 本质上只是一个 JS 对象
虚拟 DOM 的缺点
- 某些情况下,虚拟DOM的执行效率不如真实DOM操作
- 需要额外的创建函数,如 createElement 或 h,但可以通过 JSX 来简化成 XML 写法
DOM diff 是什么
DOM diff 即比较两颗虚拟 DOM 树区别的算法,记录这两棵树差异,记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。
DOM diff 可能的大概逻辑
- Tree diff
- 将新旧两棵树逐层对比,找出哪些节点需要更新
- 如果节点是组件就看 Component diff
- 如果节点是标签就看 Element diff
- Component diff
- 如果节点是组件,就先看组件类型
- 类型不同直接替换(删除旧的)
- 类型相同则只更新属性
- 然后深入组件做 Tree diff(递归)
- Element diff
- 如果节点是原生标签,则看标签名
- 标签名不同直接替换,相同则只更新属性
- 然后进入标签后代做 Tree diff(递归)
DOM diff的问题(key)
举个Vue的例子(代码),在3个 input 依次输入三角形、正方形和圆形
然后点击中间的 delete 按钮,你会看到结果:
令人疑惑的在于原来的第三项「圆形」消失了!「正方形」却被保留下来了!
原因
你认为你删除了2,但Vue会认为你做了两件事:
- 把2变成了3
- 然后把3删除了 Vue 为什么要舍近求远呢?看看这两个数组:[1,2,3] 和 [1,3],人类会说,这不就是少了个 2 吗?
但是计算机会怎么对比数组?遍历!
首先对比 1 和 1,发现「1 没变」;然后对比 2 和 3,发现「2 变成了 3」;最后对比 undefined 和 3,发现「3 被删除了」。
所以计算机的结论是:「2 变成了 3」以及「3 被删除了」,所以再看之前结果就很合理了。
破解之法 --- 用一个唯一id作为 key
为什么不能用 index 作为 key
如果你用 index 作为 key,那么在删除第二项的时候,index 就会从 1 2 3 变成 1 2(因为 index 永远都是连续的,所以不可能是 1 3),那么 Vue 依然会认为你删除的是第三项。也就是会遇到上面一样的 bug。