来源/参考链接:
虚拟DOM
虚拟DOM 是什么
一个能代表 DOM 树的
JS对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性。
虚拟DOM 长什么样
Vue的虚拟DOM
const vNode={
tag: 'div',//标签名或组件名
data: {
class:'red',//标签上的属性
on:{
click:()=>{} // 事件
}
},
children:[//子元素们(这里表示div下有两个span)
{ type:'span', ... },
{ type:'span', ... }
],
}
React 的虚拟DOM
const vNode={
key=null,
type='div',//标签名或组件名
props:{
children:[//子元素们(这里表示div下有两个span)
{ type:'span', ... },
{ type:'span', ... }
],
className:'red',//标签上的属性
onclick:()=>{} // 事件
},
ref=null
}
如何创建虚拟DOM
Vue
- 方法1(麻烦):用函数创造虚拟 dom
- 方法2(简便):直接用模板来创造虚拟DOM。再用 vue-loader 转译 。
React
- 方法1(麻烦):用 React.CreateElement 创造虚拟 dom
- 方法2(简便):直接用 JSX 来创造虚拟DOM。JSX 可以用 babel 转换成 createElement 的形式。
简便形式的缺点:严重依赖打包工具
虚拟DOM的优缺点
虚拟DOM的优点
操作虚拟DOM带来的性能提升主要是来自这 2 个方面:
- 虚拟DOM 能够减少不必要的 DOM 操作。
-
虚拟DOM可以将多次操作
合并为一次操作(减少次数) 比如,添加 1000个dom节点,原生 js 要每次一个一个添加 1000 次,但是 react/vue 可以把这 1000 操作合并成一次,把 1000 次操作直接封装在一个数组里面,一次性更新 dom。 (不是优化 dom 操作,而是优化 dom 操作的次数) -
虚拟DOM可以借助DOM diff 省去多余的操作。(减少范围) 比如,页面上有 990 个节点,还要添加 10 个。原生 js 会操作 1000 次 dom,但是使用虚拟 dom 会仅仅更新新添加的 10 个 dom。因为虚拟 dom 发现那些 990 个节点已经在页面里面了,它并不会去更新它们。
- 虚拟DOM 能够跨平台渲染
虚拟 DOM 本质上是一个 js 对象,它不仅可以变成DOM,还可以给任何其他实体建立映射关系,比如:ios 应用,安卓应用,小程序。
虚拟DOM的缺点
- 需要用额外的创建函数,如 CreateElement (react)或h(vue),可以用 JSX 来简化成XML写法,
严重依赖打包工具,因为 JS 不认识 jsx 语法。
操作真实 dom 慢,是这样吗?
- 和操作原生 js相比 ,操作 dom 要比操作原生 js 里面的数组确实要慢。(操作 dom 本身其实并不慢,操作 1000 个 dom 在毫秒级)
- 任何基于 DOM 的库(比如Vue/React)都不可能在操作DOM的时候比 DOM 还快。 比如说 vue 和 react,因为这两个库的底层还是操作真实 dom,相当于加了一层。也就是说,并不能简单说,操作虚拟 dom 本身会提升浏览器性能。
虚拟DOM 和真实DOM 性能对比
数据规模合理时,比如小于几千时,虚拟DOM可以省去多余的操作。但是规模大到很大程度时,比如十万级别,真实DOM 会更加稳定。如果 react 没有任何优化,10 万级别的DOM 会在浏览器上造成 30s 左右的不可交互时间。vue 两秒左右
DOM diff——虚拟DOM的对比算法
diff算法是发生在虚拟DOM上的
例2,删除了左子树
请注意,dom diff 并不是简单的删除左子树(计算机是从左往右对比的),而是:
- 比较首层,发现 div 没变,不更新
- 比较左边的 span,发现 children 改变,更新 dom 内容
- 比较右侧 span,发现 span 被删除,batch 记录删除 dom
diff 算法是什么
本质就是一个函数,输入参数是两个虚拟 dom,输出参数是一个补丁,补丁队列就是对真实 dom 的增量。
patches = patch(oldVNode, newVNode)
补丁队列长这样:
[
{type:'INSERT',vNode:...},
{type:'TEXT',vNode:...}, //更新文本
{type:'PROPS',propsPatch: []} //更新属性
]
DOM diff 的大概逻辑
graph TD
Tree-diff逐层比较,找出需要更新的节点 --> 判断节点类型
判断节点类型 --> 组件:Component-diff
判断节点类型 --> 标签:Element-diff
组件:Component-diff--> 比较组件类型
比较组件类型-->类型不同
比较组件类型-->类型相同
类型不同 --> 直接替换
类型相同--> 只更新属性
标签:Element-diff--> 比较标签类型
比较标签类型-->类型不同
比较标签类型-->类型相同
直接替换--> 深入组件做Tree-diff递归
只更新属性--> 深入组件做Tree-diff递归
深入组件做Tree-diff递归 --> Tree-diff逐层比较,找出需要更新的节点
Tree diff
- 将新旧两棵树逐层对比,找出需要更新的节点
- 如果节点是组件,执行 Component diff
- 如果节点是标签,执行 Element diff
Component diff
- 如果节点是组件,先比较双方类型是否一致(比如弹框组件变成轮播组件)
- 类型不同直接替换(删除旧的)
- 类型相同则只更新属性
- 然后深入组件做Tree diff(递归)
Element diff
- 如果节点是原生标签,比较标签名是否一致,
- 不同直接替换,相同则只更新属性
- 然后深入标签做Tree diff(递归)
dom diff 有一个显著的缺点:vue 横向比较存在 bug
Vue2.0 v-for 中 :key 到底有什么用?226 关注 · 23 回答问题
bug 的具体例子
[1,2,3] 对比 [1,3]
- 1 和 1 对比,没变化,继续
- 2 和 3 对比,把 2 变成 3,只改变了标号,并未改变内容,因为 vue 认为这两个是一个对象,仅仅是属性不一样而已
- 3和空,对比,把3删除(包括子元素 rabbit)
解决方案:加了 key 没 bug
[{id:1, value:1}][{id:2, value:2}][{id:3, value:3}]
[{id:1, value:1}][{id:3, value:3}]
- 比较第一个元素,没有变化。
- 比较第二个元素,发现 id = 2 的元素已经被删除。vue 不再认为这两个是一个对象,因为 key (id) 的值并不相同,它会进行整体的替换。
- 比较一个队列的第三个元素和第二个队列的第二个元素,发现相同,不进行更改。
默认就是 key 就是 index,绝对不可以用 index 当做 key。
在 react 中,上述的 bug 虽然不会出现,但是不使用 key 会对效率产生严重的影响:
当没有使用 key 的时候
第一个 list 中 index = 2 插入的 f,会导致三次替换操作。
使用 key 的时候,由于新加入的 f key 值不同,算法会直接输出 插入f 到 patch 中,然后指在 f 上的指针会右移到 C 上并继续比较,没有任何替换操作。