React新旧DOM和key的关系

2024-08-02
18877 分钟
...

我们都知道,react是通过新旧DOM对比,进行更新渲染的。这样一方面可以积累多次更新到一次渲染中,减少渲染次数;另一方面通过新旧DOM的对比,尽可能复用之前的组件实例,提示性能。这里我主要说一下新旧DOM的对比以及与key的关联性。

  1. 新的虚拟DOM是基于组件的最新propsstate的值计算得出的,然后再和旧DOM进行比较。这里我想强调的是新的DOM不是基于旧的DOM生成的
  2. react会给每一个DOM元素,优先使用开发自定义的key,没有的话会提供默认的key,这个key是基于该组件在父组件中的索引,比如第一个就是“0”,第二个就是“1”。所以,默认的key是和位置一一对应的。
  3. 虽然keyreact用来识别组件用的,但是react并不会把它作为唯一的标识。比如即使父组件Father下第一个索引位置,第一次渲染的是_组件A_,第二次渲染的是_组件B_。这两个组件不是同一个组件类型,那即使他们都处于同一个索引下,即使给他们定义了同一个keyreact也不会认为是同一个组件。总的来说,判断前后是否为相同组件是key+组件类型。下面刚好有几个例子论证了这个观点。
  4. showBfalse时没有第二个节点,showBtrue时,由于当前位置原本没有节点,只能重建,而不会复用同层key为"1"的组件实例(不同位置上)。
<div>
    <Counter key="1"/>
    {showB && <Counter key="1"/>} 
</div>
  1. key只在兄弟组件中有效,所以,如果一个父组件只有一个子组件,那怎么定义该子组件的key,都无所谓,因为不会出现复用导致的混乱(列表组件容易出现)。
  2. 组件A = () => {return null}{show ? <A/> : null} 不是一回事。前者不会销毁_组件A_,当前位置仍然是_组件A_的,一旦状态或者props发生变化,组件A_会立刻更新的,直接销毁再创建肯定消耗大;后者show=false时直接销毁_组件A,因为该位置的组件由_组件A_换成了组件null,这两者不是一个组件类型。所以,想要组件不显示的时候销毁避免再次显示时复用之前的state,可以通过{show ? <A/> : null}
  3. 相同位置(意味着相同的默认key)的相同组件,就会复用,比如下面示例:
{isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}

这种情况,isFancytrue转为false的时候,会复用第一个,因为比较发现前后是相同组件。如果给他们加上不同的key,就不会复用了。

  1. 相同位置的不同组件,所以不会复用,比如下面示例:
{isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}

这种情况,isFancytrue转为false的时候,不会复用,因为比较发现前后不是相同组件(一个是div,一个是section),即使提供给他们加上同一个key也没有用。

  1. 为什么列表中不能用index作为key或者不提供key(默认的key也相当于使用index)? 因为列表中组件类型一样,index如果也一样的话,就会复用,导致组件没有彻底刷新。参考1 可以看出,这种相同位置的复用,导致我们即使更换列表顺序也影响不了内容的顺序,这并不是列表组件想要的结果。
  2. 复用是在同层中,去找相同key和相同组件类型的DOM元素,找到了就复用。参考4 提到了列表组件,key提升性能。示例如下:

 * 1代码运行在https://codesandbox.io/s/gl9r8m?file=/src/App.js&utm_medium=sandpack,
 * 2点击切换和点击li可以看到组件的复用情况同时可以修改nameid和key观察组件复用情况!!
 */
import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
    </div>
  );
}

function Counter() {
  const [change, setChange] = useState(false);
  return (
    <div className="container">
      <button onClick={() => setChange((pre) => !pre)}>切换</button>
      {change ? (
        <>
          <Item key="1" id="1" name="一" />
          <Item key="2" id="2" name="二" />
          <Item key="3" id="3" name="三" />
        </>
      ) : (
        <>
          <Item key="3" id="3" name="三" />
          <Item key="2" id="2" name="二" />
          <Item key="1" id="1" name="一" />
        </>
      )}
      {}
      {change ? (
        <>
          <Item key="1-a" id="1" name="一" />
          <Item key="2-a" id="2" name="二" />
          <Item key="3-a" id="3" name="三" />
        </>
      ) : (
        <>
          <Item key="3" id="3" name="三" />
          <Item key="2" id="2" name="二" />
          <Item key="1" id="1" name="一" />
        </>
      )}
    </div>
  );
}


 * 提供一个状态监测重新渲染后的状态是否保留
 * 注意key React 内部保留的一个特殊属性不会传递给组件
 * */

const Item = ({ id, name }) => {
  const [state, setState] = useState('');
  return (
    <div>
      <li key={id} onClick={() => setState(name)}>
        {name} - {state}
      </li>
    </div>
  );
};
  1. 给一个组件固定一个key会怎么样?显然结果就是,只要组件还在,就会一直复用!!旧DOM在该层有这个key(比如key="XX")的组件,根据propsstate生成的新DOM在该层也有这个key(因为节点同样用了这个组件提供的key)。那react对比发现前后有相同的key和相同的组件类型,那必然复用。好处就是前面说的列表组件,提供唯一的key提升性能,坏处就是会一直保留,除非父组件销毁过。
  2. 组件实例是一个对象,存有组件的state,渲染后会存在于某个内存中。react要复用就直接使用该内存下的该组件对象实例。组件复用是复用整个组件实例。如果react不主动销毁,它可能被JavaScript的垃圾回收机制回收(没有其他引用指向该组件实例),否则它就会在内存中让你后面直接用。

总结下来,react复用组件的依据:key+组件类型。不一定准确,但至少在我遇到的场景中是得到验证的!

参考

  1. react的key详解
  2. react对state的保留和重置
  3. react渲染顺序和useEffect执行顺序
  4. 虚拟DOM的diff算法

参考1 可以验证,DOM对比根据key+组件类型,判断前后是否为相同组件。列表组件就是因为这二者一致导致每一项都被复用,状态保留,所以无论如何更换列表顺序,内容顺序没有发生变化;

参考2react官方资料,在强调相同位置的状态保留。由于没有指定key,所以默认提供的key就是索引,与位置一致,索引位置相同也意味着key相同,再加上相同组件类型相同,所以会复用组件实例。自己也可以尝试额外加key参数,不同的key会导致不再复用。

参考3react渲染顺序是从上到下。毕竟reactprops传递也是由上到下的(useContext另说),父组件渲染后,子组件会根据父组件的一些条件比如根据父组件以及自身在父组件中索引位置判断是否复用,比如props确定渲染内容。在所有组件都渲染完成后React会开始执行副作用和生命周期方法,这是从下到上的,子组件通常是父组件逻辑的一部分。

参考4 是_diff_算法,同层比较的方式,进行DOM更新。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-12-02
useEffect与useLayoutEffect对比
在 React Hooks 中, 和 都用于处理副作用逻辑,例如订阅、事件监听、DOM 操作等。但它们有着本质性的执行时机差异,这影响到页面渲染、布局抖动、性能表现等关键点。 1\. 执行时机的核心区别 在 React 的渲染流程中: 1. 渲染(Render phase) React 根据 state/props 计算 UI,生成虚拟 DOM,不访问真实 DOM。 2. 提交(Commit phase) React 将虚拟 DOM 变...
学习
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2025-07-30
React性能优化三剑客:memo、useMemo和useCallback详解
前言 在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具: 、 和 ,它们如何帮助我们构建更高效的React应用。 1\. 为什么需要性能优化? 🤔 React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的: 父组件更新导致所有子组件重新渲染 ,即使子组件的props没有变化 复杂计算在每次渲染时重复执行 ,消耗大量资源 函数引用在...
学习面试
react2025-06-05
React 中 useDeferredValue 和 startTransition 的核心区别与使用场景
React 中 和 ,两者都用于优化性能,但适用场景和实现方式不同。 核心概念解析 1\. useDeferredValue:延迟值更新 作用 :告诉 React 延迟更新某个值 ,直到所有高优先级渲染任务完成。 适用场景 :当某个值的更新会触发计算密集型渲染(如复杂列表、图表),但需要优先保证其他高优先级 UI(如输入框)的响应。 机制 : 保持旧值显示,后台计算新值。 计算完成后,用新值更新 UI,期间允许用户继续交互。 示例 :...
学习
react2024-12-18
Antd 样式覆盖
&nbsp; 目前作者所在的业务正在升级 Antd5.0,不得不说 Antd5.0 真的太香了,但是由于团队的 UE 规范进行了大改版,Antd5.0 所有的 必须对齐最新的规范,因此就需要对 Antd5.0 组件做主题定制。 现状 目前针对项目中使用的 antd 高频组件进行了梳理,并且根据规范基于 design token 对这些高频组件进行样式定制,对于主题色、圆角、边框、字体等用户 高的主题都已经满足团队规范。 虽然 Desig...
学习
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录