前端基础知识之虚拟 DOM 和 DOM diff

145 阅读3分钟

今天我们来总结一下有关于虚拟 DOM 和 DOM diff 的相关知识点。

首先 DOM 相信大家都不会陌生,就是文档对象模型,这是 JS 中的一个重要的概念。那么虚拟 DOM 又是什么呢?

虚拟 DOM

虚拟 DOM 是一种能够表示 DOM 树的对象,通常会含有签名、标签上的属性、事件监听和子元素等。

它有如下的优点:

  1. 能够减少 DOM 操作。虚拟 DOM 能将多次操作合并成一次操作,以此达到优化操作次数的目的
  2. 优化操作范围。虚拟 DOM 能够借助 DOM diff 把多余的操作省略掉,以此达到优化操作范围,选出实际需要操作的对象的目的
  3. 跨平台操作。虚拟 DOM 不单止可以操作普通的 HTML 元素,更可以变成 DOM、小程序等
  4. 可以通过 JSX 语法来简化成 XML 语法(需要额外的构建工具)

同时也有以下缺点:

  1. 如果规模超过一定范围,那么稳定性会较原生 DOM 更差
  2. 需要额外创建函数。比如 createElement 或者 h() 等

下面分别给出两个例子:

  • 在 React 中:
const vNode = {
	key: null,
	props:{
		children:[ //子元素们
			{type: 'span',...},
			{type: 'span',...}
		],
		className: "red" //标签上的属性
		onClick: ()=>{} //事件
	},
	ref: null,
	type:"div", //标签名 or 组件名
	...
}

//创建虚拟dom
createElement('div',{className:'red',onClick:()=>{}},
	[
	createElement('span',{},'span1')
	createElement('span',{},'span2')
	]
)

//JSX简化,通过 babel 转为 createElement的形式

<div className="red" onClick={()=>{}}>
	<span>span1</span>
	<span>span2</span>
</div>
  • 在 Vue 中:
const vNode = {
	tag:"div", //标签名 or 组件名
	children:[
		 //子元素们
		{type: 'span',...},
		{type: 'span',...}
	],
	data:{
		class: "red" //标签上的属性
		on:{
			click: ()=>{}
			} //事件
	},
	...
}

//创建虚拟dom
h('div',{
	class: 'red',
	on:{
		click:()=>{}
	},
},[h('span',{},'span1'),h('span',{},'span2')])


//Template简化,通过vue-loader 转为h形式
h('div',{
	class: 'red',
	on:{
		click:()=>{}
	},
},[h('span',{},'span1'),h('span',{},'span2')])

DOM diff

DOM diff 其实就是虚拟 DOM 的对比算法,会对比两颗虚拟 DOM 树的所有节点,并生成一个 patch,用来局部更新真实的 DOM

其中,patches = patch(oldVNode, newVNode)

它最重要的作用,就是找出新旧两个节点或 DOM 树的不同,然后对不通的节点进行操作,实际的 DOM 操作可能是:

[
	{type: 'INSERT', vNode:...},
	{type: 'TEXT', vNode:...},
	{type: 'PROPS', propsPatch:[...]},
]

那么要如何确切的理解呢?

我们可以把虚拟 DOM 想象成一个树形:

image.png

如果我们只改变根节点的 div 的 class 属性的话,那么 DOM diff 就会发现只有 div 的 class 改变了,那么他就只会修改这一点,两个 span 和里面的内容都不会被改变

但是如果我们删掉图中的第二个 span,那么奇怪的事情就发生了,DOM diff 会认为是把第一个 span 的内容修改成第二个 span 的内容,然后删掉第二个 span,也就是变成这样:

image.png

那么可以稍微理解成,DOM diff 可能有如下三种逻辑:

  1. three diff
  • 将新旧两棵树对比,找出需要更新的节点

  • 如果节点是组件,就看Component diff

  • 如果节点是标签,就看 Element diff

  1. Component diff
  • 如果节点是节点组件,就先看组件类型

  • 类型不同直接替换,删掉旧的

  • 类型相同则只更新属性

  • 然后深入组件做Three diff(递归)

  1. Element diff
  • 如果节点是原生标签,则看标签名

  • 标签名不同直接替换,相同则只更新属性

  • 进入标签后再做Three diff(递归)

但是 DOM diff 也有缺点,那就是如果是同级比较的话,就会存在 bug,详情可以参考方应杭举的非常生动的例子

那么要解决这个 bug,就需要在每个变量后面加上对应的 key,而且千万不能是 index!

©本总结教程版权归作者所有,转载需注明出处