React DOM元素每次重渲染触发挂载卸载而非更新的原因问询
你观察到的callback ref每次渲染都被调用的现象,并不代表table对应的DOM节点真的被卸载重建,和React协调的同类型节点复用规则没有冲突,本质是你对callback ref的调用触发场景理解不全,加上内联函数的引用特性共同导致的。
核心规则补充
你之前参考的官方文档只说明了两个最基础的ref调用时机:
组件挂载时,React会向ref回调传入对应DOM元素进行调用;组件卸载时,会传入null调用该ref回调。
但文档同时隐含了一个非常关键的触发场景,很多开发者都会忽略:
如果两次渲染间传入的ref回调函数本身的引用发生了变化,React会先传入
null调用上一次渲染的旧ref回调做清理,再传入当前DOM节点调用新一次渲染的新ref回调。
你的代码里给table传的ref是直接写在JSX里的内联箭头函数:
<table ref={(ref) => console.log(ref)}>
每次组件因为num状态变化触发重渲染时,这个箭头函数都会被重新创建,生成一个全新的函数引用,和上一次渲染用到的ref回调完全不是同一个函数。React检测到ref属性的引用变更,就会按照规则先执行旧回调的清理逻辑(传null),再执行新回调传入当前的table DOM节点,所以你每次点击按钮都会看到两条日志:先输出null,再输出table节点,很容易误以为DOM被卸载重建。
验证方式
你可以通过简单的对比确认DOM节点从未被替换:在ref回调里把拿到的DOM节点存在一个外层变量里,下次回调拿到新的节点时做全等对比,就会发现每次传入的table节点都是同一个引用,底层DOM完全没有走卸载、重挂载的流程,只是ref回调被替换触发了调用而已。
避免重复触发的方案
如果你不希望每次重渲染都触发ref回调,只需要保证ref回调的引用在多次渲染间保持稳定即可,有两种常用实现方式:
- 用
useCallback包裹ref回调,设置空依赖数组保证引用不变:
这种写法下,ref回调只会在组件首次挂载时传入DOM节点调用一次,组件卸载时传入null调用一次,中间的重渲染不会触发回调。const stableTableRef = useCallback((node) => { console.log(node); }, []); // JSX中使用 <table ref={stableTableRef}> - 直接使用
useRef创建ref对象替代callback ref,ref对象本身的引用是永久稳定的,不会出现重复触发的问题:const tableRef = useRef(null); // 需要访问DOM节点时直接取tableRef.current即可 // JSX中使用 <table ref={tableRef}>
常见误区提醒
不要把callback ref传入null的场景和DOM卸载直接划等号:只有当传入的ref回调是稳定引用时,收到null才代表对应的DOM节点真的被卸载;如果是每次渲染都重新创建的内联ref回调,收到null只是React执行新旧ref回调切换的清理流程,和DOM本身的生命周期无关。
内容的提问来源于stack exchange,提问作者fast-reflexes

