虚拟DOM是什么
一个能代表DOM树的形象,通常含有标签名、标签上的属性,事件监听和子元素们,以及其他属性
关于DOM的谣言
DOM操作慢?虚拟DOM快
-
这句话类似于:刘翔矮(对比于姚明)
-
DOM操作慢是对比于JS原生API,如数组操作
-
任何基于DOM的库(Vue/React)都不可能在操作DOM时比DOM快
为什么存在这些谣言?因为在某些情况下。虚拟DOM快
虚拟DOM优点
减少DOM操作(两个例子)
-
(减少DOM操作的次数) 虚拟DOM可以将多次操作合并为一次操作,比如你添加1000个节点,却是一个接一个操作的(DOM操作1000次,虚拟DOM只需要操作一次)(不是优化DOM操作,而是优化DOM操作的次数)
-
(减少DOM操作的范围) 虚拟DOM借助DOM diff可以把多余的操作省掉,比如你添加1000个节点,其实只有10个是新增的(如果发现有一些是已经在页面里的,不需要更新,它就不更新
跨平台
- 虚拟DOM不仅可以变成DOM,还可以变成小程序、iOS应用、安卓应用,因为虚拟DOM本质上只是一个JS对象
虚拟DOM长什么样子
React
const vNode = {
key: null,
props: {
children: [ //子元素们
{ type: 'span', ...},
{ type: 'span', ...}
],
className: "red" //标签上的属性
onClick: ()=> {} //事件
},
ref: null,
type: "div", //标签名 or 组件名
...
}
这个对象就表示了一个标签为div,子元素为两个span,className为red,点击事件调用函数的一个DOM
Vue
const vNode = {
tag: "div", //标签名 or 组件名
data: {
class: "red", //标签上的属性
on: {
click: () => {} //事件
}
},
children: [ //子元素们
{ tag: "span", ...},
{ tag: "span", ...}
],
...
}
这个虚拟DOM, Vue里面表示div,class为red,onclick执行一个函数,子元素是两个span
如何创建虚拟DOM
React.createElement
createElement('div', {className: 'red', onClick: () => {}}, [ createElement('span', {}, 'span1'), createElement('span', {}, 'span2') ]
)
Vue (只能在render函数里得到h)
h('div', {
class: 'red',
on: {
click: () => { }
}
}, [h('span', {}, 'span1'), h('span', {}), 'span2'])
用JSX简化创建虚拟DOM(现在创建虚拟DOM的方法)
React JSX
<div className="red" onClick="{()=>{}}">
<span>span1</span>
<span>span2</span>
</div>
通过 babel转为 createElement 形式
为什么React可以用babel来转换?
因为React团队和babel团队关系很好,所以babel团队就把React JSX语法直接作为插件内置了。使用最新的配置就不需要再配置就可以支持。
Vue Template
<div class="red" @click="fn">
<span>span1</span>
<span>span2</span>
</div>
通过 vue-loader转为 h 的形式
为什么vue不能用babel来转换?
因为两个团队互不认识。Vue使用的是单文件(.vue),它不是JS,不是原生的JS语法,不能用JS来理解.vue,只能通过vue-loader。而React默认的就是JS
虚拟DOM有什么缺点
-
需要额外的创建函数,如createElement或h,但可以通过JSX来简化XML写法
-
React JSX 和 Vue Template两种方法严重依赖打包工具,不打包的话,是不认识标签的意思,需要额外的添加构件的过程。
DOM操作并不慢,只是渲染慢
JS用时是很快的,但是浏览器在渲染页面的时候,可让页面不可交互
DOM操作快还是React操作快?
结论:不确定,当规模小的时候,一定是React优化的更好,它会减少不必要的操作和bug;当规模到达很大的时候,它的额外的计算会使自己变慢。
结论1:当你的数据规模是合理的范围,只有几千的话,用虚拟的DOM是很好的,可以很好的优化多余的操作或者是没有优化的操作;当时当你的规模大到一定的范围,在这种极限的情况下,原生的DOM会保证一定的稳定性(Vue会默认的做一些优化)
DOM diff
虚拟DOM的对比算法
什么是DOM diff
-
就是一个函数,我们称之为patch
-
patches = patch(oldVNode, newVNode)
-
patches就是要运行的DOM操作,可能长这样:
[
{type: 'INSERT', vNode: ...},
{type: 'TEXT', vNode: ...}
{type: 'PROPS', propsPatch: [...]}
]
比对(diff) 渲染更新前后产生的两个虚拟dom对象的差异,并产出差异补丁对象,再将差异补丁对象应用到真实dom节点上
DOM diff可能的大概逻辑
Tree diff
- 将新旧两棵树逐层对比,找出哪些节点需要更新
- 如果节点是组件就看Component diff
- 如果节点是标签就看Element diff
Component diff
- 如果节点是组件,就先看组件类型
- 类型不同直接替换(删除旧的)
- 类型相同则只更新属性
- 然后深入组件做 Tree diff(递归)
Element diff
- 如果节点是原生标签,则看标签名
- 标签名不同直接替换,相同则只更新属性
- 然后进入标签后代做Tree diff(递归)
DOM diff的缺点
- 同级节点对比存在bug
DOM diff 中的key问题
key的作用主要是为了高效的更新虚拟DOM。
diff算法是从左往右进行同层级对比的,如果发现元素相同但是内容不相同,会直接修改内容。解决的方法就是加上唯一的 key,让 Diff 知道就算是同类型的组件,也是有名字区分的,更新视图就不会出错。
如果没有key值,就会根据就地复用的原则,一个一个对比,然后修改渲染,场景:在同一层级的某一堆节点中插入一个新节点。如果有key,diff算法就可以通过对比找到正确的位置插入新节点,而key值相同的dom节点就不要去比较。
如果key值用index,假如我在数组中间插入一项的时候,此时从这一项开始的key值就全部都变了,都需要重新对比渲染。因此,复杂的列表不要用index。