React中的虚拟DOM和diff算法详解
React中的虚拟DOM和diff算法详解
React中的虚拟DOM和diff算法是其高效更新UI的核心机制。虚拟DOM通过在内存中对比差异,最小化对真实DOM的操作,提高了应用的性能和响应速度。本文将详细介绍虚拟DOM的工作原理、优势以及diff算法的三大策略。
虚拟DOM
结构对比
虚拟DOM实际上是一个对象的结构,React通过该对象将真实DOM渲染出来。真实的DOM上会挂载很多默认的属性和方法,而虚拟DOM上并没有这些属性。由于我们实际上不需要关注这些属性,因此虚拟DOM比真实DOM更轻,运行起来更快。
流程对比
传统的DOM结构,只要数据发生变化就会实时地更新到用户界面中,这会导致频繁的DOM渲染,产生很大的性能消耗。而虚拟DOM则将所有的操作聚集到一块,计算出所有的变化后,统一更新一次虚拟DOM,再触发页面的更新,整个过程页面只重新渲染一次。
从上述描述可以看出,越是复杂的页面,或者DOM操作频繁的情况下,虚拟DOM的优势就越大。
虚拟DOM的主要部分
对于下面这段结构:
<div className='Index'>
<div>虚拟 dom</div>
<ul>
<li>React</li>
</ul>
</div>
在React转化为一个虚拟DOM对象:
{
type: 'div',
props: { class: 'Index' },
children: [
{
type: 'div',
children: '虚拟 dom'
},
{
type: 'ul',
children: [
{
type: 'li',
children: 'Vue'
},
]
}
]
}
虚拟DOM主要包含三部分:
type
:实际的标签props
:标签内部的属性(除key和ref,它们会形成单独的数据结构进行处理)children
:节点内容,依次循环
可以看出虚拟DOM的结构非常简单清晰。
虚拟DOM的优势
提高效率
使用原生JS时,我们需要关注如何操作DOM,怎样更新DOM。而React通过虚拟DOM确保DOM的匹配,让我们可以更加关注业务逻辑,从而提高开发效率。
性能提升
React会将整个DOM保存为虚拟DOM,如果有更新,都会维护两个虚拟DOM,以此来比较之前的状态和当前的状态,并确定哪些状态被修改,然后将这些变化更新到实际DOM上。一旦真正的DOM发生改变,也会更新UI。
在React源码中,createElement
函数是创建虚拟DOM节点的关键:
// 创建一个 React 元素(虚拟 DOM 节点)的函数
function createElement(type, config, children) {
// 用于遍历 config 对象属性的变量
let propName;
// 用于存储 React 元素属性的对象
const props = {};
// 用于存储 React 元素的 key
let key = null;
// 用于存储 React 元素的 ref
let ref = null;
// 如果传入了 config 对象
if (config!= null) {
// 如果 config 对象中有有效的 ref
if (hasValidRef(config)) {
// 将 config 对象中的 ref 赋值给 ref 变量
ref = config.ref;
}
// 如果 config 对象中有有效的 key
if (hasValidKey(config)) {
// 将 config 对象中的 key 转换为字符串并赋值给 key 变量
key = '' + config.key;
}
// 遍历 config 对象的所有可枚举属性
for (propName in config) {
// 如果 config 对象自身拥有该属性,并且该属性不在 RESERVED_PROPS 对象中(RESERVED_PROPS 可能包含一些 React 保留的属性名)
if (
hasOwnProperty.call(config, propName) &&
(!RESERVED_PROPS.hasOwnProperty(propName))
) {
// 将 config 对象中的属性添加到 props 对象中
props[propName] = config[propName];
}
}
}
// 计算传入的子元素数量
const childrenLength = arguments.length - 2;
// 如果只有一个子元素
if (childrenLength === 1) {
// 将唯一的子元素直接赋值给 props.children
props.children = children;
}
// 如果有多个子元素
else if (childrenLength > 1) {
// 创建一个长度为子元素数量的数组
const childArray = Array(childrenLength);
// 遍历每个子元素,将其依次存入数组
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 返回一个描述 React 元素的对象
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
......
};
}
这个函数将组件的类型(比如 div、span 或者自定义组件)、属性以及子元素等信息组合成一个虚拟DOM节点对象。React通过递归调用createElement
,把整个组件树构建成一个完整的虚拟DOM树。
在React中,需要维护两个完全独立的虚拟DOM,在每次更新时,生成一个新的虚拟DOM树,然后和之前的虚拟DOM树进行对比。这一对比过程主要由协调算法(reconciliation)完成。在React中主要通过reconcileChildFibers
函数协调:
function reconcileChildFibers(
// 当前正在处理的父 Fiber 节点
returnFiber,
// 当前父节点的第一个子 Fiber 节点
currentFirstChild,
// 新的子节点
newChild,
// 优先级相关的概念,用于标记任务的优先级
lanes
) {
// 检查 newChild 是否是一个顶层无 key 的片段。如果是,则将 newChild 替换为其 props.children,当作普通数组处理
if (newChild === null) {
if (currentFirstChild!== null) {
newChild = newChild.props.children;
}
return null;
}
// 如果 newChild 是一个对象,根据 $$typeof 属性的值进行不同处理。如果当前值不满足已有条件,调用 throwOnInvalidObjectType 抛出错误。
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 放置该子节点
return placeSingleChild(
// 处理单个元素
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
// 处理单个门户
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
// 懒加载组件,获取其 payload 和 init 函数,调用 init(payload) 得到实际的子节点,然后递归调用 reconcileChildFibers 处理
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
// 数组
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 迭代器
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// 处理字符串和数字类型的新子节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 一系列对比和更新逻辑
//...
// 对于其他未处理的情况,将剩余的子节点当作空处理,调用 deleteRemainingChildren 删除当前父节点下剩余的子节点。
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
这个函数会遍历新旧虚拟DOM树的节点,比较它们的差异,确定哪些节点发生了变化。并会确定哪些状态被修改,然后将这些变化更新到实际DOM上。在找出虚拟DOM树之间的差异后,React会生成一系列的DOM更新操作,这些操作会被应用到实际的DOM上。一旦真正的DOM发生改变,也会更新UI。当React通过上述过程对真实DOM进行修改后,浏览器会自动检测到DOM树的变化,然后重新计算样式布局(回流和重绘),将最新的DOM状态展示到屏幕上,从而实现UI的更新。这是浏览器的原生机制,React依赖此机制确保页面随着数据变化实时更新。
总结来说,虚拟DOM和协调算法是React高效更新UI的核心机制,通过在内存中对比虚拟DOM差异,最小化对真实DOM的操作,提高了应用的性能和响应速度。
兼容性
React具有超强的兼容性,可分为:浏览器的兼容和跨平台兼容
- React基于虚拟DOM实现了一套自己的事件机制,并且模拟了事件冒泡和捕获的过程,采取事件代理、批量更新等方法,从而磨平了各个浏览器的事件兼容性问题
- 对于跨平台,React是根据虚拟DOM画出相应平台的UI层,只不过不同的平台画法不同而已
diff算法
diff算法就是通过循环递归对节点进行依次对比。在React主要通过三大策略完成了升级优化:
- Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一id进行区分
分别对应:
- tree diff:同级比较,React通过updateDepth对VirtualDOM树进行层级控制,也就是同一层,在对比的过程中,如果发现节点不在了,会完全删除不会对其他地方进行比较,这样只需要对树遍历一次就OK了
- component diff:组件比较:对同种类型组件对比,按照层级比较继续比较虚拟DOM树即可;对于不同组件来说,React会直接判定该组件为dirty component(脏组件),无论结构是否相似,只要判断为脏组件就会直接替换整个组件的所有节点
- element diff:节点比较:React对于同一层级的一子自节点,通过唯一的key进行比较;
- 在比较节点的时候,通过比较index(当前节点在旧节点中的位置)和lastIndex(对比的索引),lastIndex一开始是默认的0,当每次比较后,会改变对应的值,也就是lastIndex=(index, lastIndex)中的最大值,如果满足这个条件,就将旧节点移动到lastIndex的位置;
- 如果index不存在,新增节点,lastIndex不变;
- 遍历完新节点后,删除旧节点中多余的节点;
注意:节点比较是通过key值进行比较的,需要保证key的唯一性,使用index做key值的话,会导致每一个节点都找不到对应的key,导致所有的节点都不能复用,都会重新创建。