虚拟DOM
什么是虚拟DOM?
虚拟DOM是一个JS对象树,用于描述真实DOM的结构和属性,包含标签类型,属性props和子元素children的对象。【虚拟DOM可以是对象,数组,字符串。。。】
$$typeof是一个标识虚拟DOM的符号type代表虚拟Dom对应的真实Dom类型,也有可能是React组件,Portal,Fragment。。。key是props上的key,这个属性用来在Diff判断是否需要更新ref绑定的ref(React.useRef,React.createRef)props给组件传的props
typeVNode = {
$$typeof:Symbol;
type:string;
key:string | null;
ref:unknow | null;
props:any
}
虚拟DOM工作原理(流程)
初始化阶段:React组件首次渲染时,会创建一个虚拟DOM树,该树的结构与实际的DOM结构一一对应。JSX解析:React中,使用JSX语法描述虚拟DOM结构。转译为React.createElement函数的调用,创建虚拟DOM节点。
const element = <div> Hello,React</div>
//转译后的React.createElement调用
const element =React.createElement('div',null,'Hello,React')
-
更新阶段: 当组件的状态变化时,React会重新生成新的虚拟DOM树。新的虚拟DOM树与之前的虚拟DOM树进行比较,找出变化的部分。 -
差异计算: React使用“协调算法”的策略比较新旧虚拟DOM树的差异。该算法会尽量找到最小的差异集,最小化实际DOM操作的次数。 -
更新实际DOM:找到差异后,React会根据差异集合执行相应的DOM操作,(包括节点的创建、更新、删除等操作)
React中,整体的渲染流程
-
render阶段:从虚拟DOM转换成Fiber,并对需要DOM操作的阶段打上effectTag标记
-
commit阶段:对有effectTag标记的Fiber节点进行dom操作,执行所有的effect副作用
React的commit阶段是不可中断的,在这个阶段,React会将计算好的变化应用到真实DOM上。若中断,可能会导致DOM不一致,用户看不到完整的UI更新,造成页面闪烁现象
虚拟DOM到Fiber的步骤:
- 虚拟DOM构建:React通过JSX编译生成虚拟DOM树。
- 协调(Reconciliation):React会将新构建的虚拟DOM树与前一次渲染的虚拟DOM树进行比较。React使用diff算法找出两个树之间的差异,确定哪些部分需要更新。
- 生成Fiber树:根据协调阶段确定需要更新的部分,React会生成Fiber树。
- 构建工作单元:React 将确定的更新操作分解成多个称为工作单元的任务,这些任务被组织成 effect list(副作用列表)。每个工作单元都与一个 Fiber 节点关联,包括组件的添加、更新、删除操作,以及可能的副作用(如生命周期方法的调用)。
- 执行工作单元:React 使用调度器来管理这些工作单元的执行顺序。调度器会根据任务的优先级决定执行顺序,以确保高优先级任务(如用户交互)优先执行。React 会依次执行这些工作单元,更新组件状态,执行必要的副作用。
- 更新Fiber树:在执行工作单元时,React 会根据任务的结果更新 Fiber 树。这可能包括改变 Fiber 节点的状态、创建新的 Fiber 节点、或者标记某些节点为不需要渲染。在所有工作单元执行完毕后,Fiber 树的最新状态将被用于最终的 DOM 更新。
扩展:延伸问题
为什么需要虚拟DOM?
- React框架设计:数据驱动【数据的改变---》React生成新虚拟DOM---》对比新旧虚拟DOM---〉发现差异节点---》1:有差异,更新真实DOM对应部分。2:无差异,不更新】
虚拟DOM一定比直接操作DOM快么?
不一定,简单页面中,只有几个节点,虚拟DOM的Diff和JS计算可能比直接操作DOM更耗时。
Diff算法的复杂度O(n)是怎么做到的?
通过3条规则限制对比范围:
- 只对比同一层级(不跨层)
- 节点类型不同时直接替换
- 用key标记列表项,避免全量重排
React diff
React diff三大策略
传统diff算法的复杂度是O(n^3),React通过三大策略,将复杂度O(n^3)转换成O(n)
- tree diff
- component diff
- element diff
Diff的过程---(发生在reconcile协调阶段)
-
第一次 让
新的 VDOM 和 旧的 Fiber 进行对比,看有没有能复用的节点,如果有,继续遍历,如果没有,就停止遍历。 -
判断新的 VDOM 有没有遍历完,如果遍历完,把旧的 Fiber 中剩下的节点删掉即可。若新的 VDOM 没有遍历完,则进行第二次遍历。
-
第二次遍历时,
把 旧的 Fiber 中剩下的 节点,放入一个 Map,然后遍历 新的VDOM 剩下的节点,看当前遍历的 VDOM 有没有存在于这个 Map 里面,若存在,则表明可以复用,打上更新的标记。 -
遍历完 新的 VDOM 后,旧的 Fiber 剩下的节点打上删掉标记,新的 VDOM 中新增的节点打上新增标记
React为啥不使用Vue中的双端对比算法?
因为Fiber结构上没有设置反向链表,双端diff需要向前查找节点,但每个FiberNode节点上都没有反向指针。即前一个FiberNode通过sibling属性指向后一个FiberNode,只能从前往后遍历,不能反过来。
React中的key的作用
React中的keys主要是帮助React识别和跟踪虚拟DOM中元素的变化。
核心作用
- 元素识别
Keys为列表中的每个元素提供唯一标识,使React能准确区分新增、删除或重新排序的元素,避免错误的DOM操作。 - 优化渲染性能
通过keys,React能复用已有DOM节点(而非重新创建),减少不必要的渲染开销,提升性能。 - 状态一致性
确保元素在列表变动后仍能正确保留自身状态(如表单输入值、勾选状态),防止因索引变化导致的状态错乱。
底层原理-------工作原理
在React的协调(Reconciliation)过程中,React渲染组件时,会创建一个虚拟DOM树,与之间的虚拟DOM树进行比较,找出差异,并将差异应用到真实的DOM上。
比较过程中,React对每个元素进行唯一性判断,确定是否需要更新该元素。唯一性判断就是依赖于key属性。React使用key属性的值判断元素是否相同,若两个元素的key相同,认为是同一个元素,复用之前的组件实例,减少重绘操作。
Key用于匹配新旧虚拟DOM树中的元素:
- 相同key的元素被视为可复用,React仅更新属性/状态。
- 不同key则触发销毁旧组件、挂载新组件。
React 为啥不使用index作为keys循环
在动态列表中,项目的顺序或数量会在重新渲染时发生变化。在基于“索引”实现中,数组无论怎么排序,数组中的第一项总是key="0",React在进行比较时,会认为是完全相同的项目,只是props的值不同。
用index作为key可能会引发问题?
- 对数据进行:逆序添加、删除等破坏性操作时,会产生没有必要的真实DOM更新==》效率低,影响性能
- 结构中有输入类的DOM时,会产生错误DOM更新==》界面出现问题
React有时也可以使用index作为唯一的项目id
使用场景:分页列表。