React Diff算法
根据真实dom结构生成=>虚拟dom【用对象的形式模拟dom结构】,进行比较
diff 算法 虚拟 DOM 树发生变化后,通过对比新旧虚拟DOM 树的变更差异,将更新最小差异更新到真实 DOM,只针对改变的部分进行更新,而不用整体重新渲染,以最小成本完成视图更新
React diff 算法的原理是什么?
diff算法来源于:linux的基本命令
- 比如diff one.txt two.txt。结果是展示出两个文件的不同。
- Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,对改变部分进行更新渲染,而不用重新渲染整个页面
具体流程:
- 真实的 DOM 首先会映射为虚拟 DOM;
- 当虚拟 DOM 发生变化后,就会根据差距计算生成patch,这个patch是一个结构化的数据,内容包含了增加、更新、移除等;
- 根据 patch 去更新真实的 DOM,反馈到用户的界面上。
详细说说diff算法逻辑:
-
diff算法会优先比较两棵树的根节点,如果它们的类型不同,则认为这两棵树不同,也就没必要再往下比较子节点了
- 如果两个节点类型不同或者 key 不同,那么直接将旧节点替换成新节点
- 如果两个节点类型相同,但是属性不同,那么更新旧节点的属性。
- 如果两个节点类型相同,属性也相同,但是子节点不同,那么对子节点进行递归比较
处理完根节点的对比,再去比较根节点的子节点
-
子节点的对比和根节点的对比是一样的
React 与 Vue 的 diff 算法 的区别?
相同点:
- 都用key做为唯一标识,会根据key判断元素是否要删除
- 遍历前都会根据老的节点构建一个map,方便根据key快速查找
- vue和react的diff算法都是只对同级节点进行对比,不做跨层级比较
不同点:
-
Vue diff使用双向链表, 从两头向中间进行遍历
-
React diff使用了单向链表,从左到右进行遍历
-
对静态节点的处理不一样。
- 由于 Vue 是通过 template 模版进行编译的,所以在编译的时候可以很好对静态节点进行分析然后进行打补丁标记,然后在 Diff 的时候,Vue2 是判断如果是静态节点则跳过过循环对比,而 Vue3 则是把整个静态节点进行提升处理,Diff 的时候是不过进入循环的,所以 Vue3 比 Vue2 的 Diff 性能更高效
- React 因为是通过 JSX 进行编译的,是无法进行静态节点分析的
Vue 是不提倡使用 JSX的,使用也没有问题,Vue 使用了 JSX 就没办法静态编译优化了
JSX本质是js语言,是动态的,很难做静态分析,所以无法做编译时优化,react的性能优化集中在运行时进行处理
vue template是静态的声明,可以在编译阶段优化
vdom为何用diff算法
- DOM操作是昂贵的,因此尽量减少DOM操作
- 因为DOM操作时损耗性能
- DOM的操作会触发浏览器的重绘和回流
- 找出本次DOM必须更新的节点来更新,其他的不更新
- 这个找出的过程,就是diff算法
react diff算法原理
diff 算法 虚拟 DOM 树发生变化后,通过对比新旧虚拟DOM 树的变更差异,将更新最小差异更新到真实 DOM,只针对改变的部分进行更新,而不用整体重新渲染,以最小成本完成视图更新
从源码层面分析:
在react-reconciler这个包,主要核心逻辑是diff算法、 构建虚拟 DOM 树等
-
首先diff的入口 是 reconcileChildren 会判断,如果首次渲染,current 空 null (即没有旧的Fiber树),就通过 mountChildFibers 创建子节点的 Fiber 实例。如果不是首次渲染,就调用 reconcileChildFibers去做 diff
-
React Diff核心逻辑 是reconcileChildFibers这个方法,会进行同级比较
- 在这个方法中,React会比较新旧子节点的同级节点,检查它们的类型和属性是否相同
-
节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中
-
如果两个节点类型相同,属性也相同,但是子节点不同,那么对子节点进行递归比较
- 对于多节点会通过reconcileChildrenArray方法来比较
1、如果新旧节点的key相同,属性也相同,那么这个节点可以进行复用
react diff算法主要有三个层级的策略
- tree层级
- component层级
- element层级
tree层级
- 只会对相同层级的节点进行比较
- 只要删除、创建操作,没有移动操作
- 子节点通过key区别
component层级
对于组件之间的比较
- 如果是同一个组件,会进行往下比对子节点
- 如果不是同一个组件,那么直接删除这个组件下的所有子节点,创建新的
element层级
对于比较同层级的元素
element diff 涉及三种操作:插入,移动,删除
diff相关源码方法
DOM的diff就是一个函数,称为patch
diff核心2个patch函数:
patch(container,vnode)
就是把vnode的节点信息,渲染到一个空容器中
初次渲染的时候,将vdom渲染成真正的DOM然后插入到容器里面
patch(vnode,newVnode)
再次渲染的时候,将新的vnode和旧的vnode相对比,得到最小修改,然后之间差异更新到真正的DOM树上,
patch(container,vnode)
- 通过这个函数可以让VNode渲染成真正的DOM
function createElement(vnode) {
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 给 elem 添加属性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。
elem.appendChild(createElement(childVnode)) // 递归
}) // 返回真实的 DOM 元素
return elem
}
patch(vnode,newVnode)
- 这里我们只考虑vnode与newVnode如何对比的情况:
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
// 遍历现有的children
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
// 两者tag一样
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 两者tag不一样
replaceNode(childVnode, newChildVnode)
}
}
)}
reconcileChildren
reconcileChildren 是diff的入口方法
reconcileChildren 会判断,如果首次渲染,current 空 null,就通过 mountChildFibers 创建子节点的 Fiber 实例。如果不是首次渲染,就调用 reconcileChildFibers去做 diff
reconcileChildFibers
reconcileChildFibers方法用于同级比较**、**同一节点比较
- 如果新旧子节点的类型相同,React会继续比较它们的属性(props)和子节点。子节点比较这一部分的逻辑在
reconcileChildrenArray函数中执行
reconcileChildFibers(returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, lanes: Lanes)
newChild:表示新的子节点,即新虚拟 DOM 中的子节点。
reconcileChildrenArray
reconcileChildrenArray方法用于子节点比较
- 在这个方法中,React会递归比较新旧子节点。它会尽可能地复用旧节点,以减少不必要的销毁和创建操作
- 对应 ReactElement 为数组的场景
React 的 Diff 算法不采用 Vue 的双端对比算法?
目前react diff算法是单向链表 (左到右)
- React 不能通过双端对比进行 Diff 算法优化是因为目前 Fiber架构上没有设置反向链表
Vue2 和 Vue3 都使用了双端对比算法,而 React 的 Fiber 由于是单向链表的结构,所以在 React 不设置由右向左的链表之前,都无法实现双端对比