深入 React 原理:Reconciliation

写在前面:从 Vue 转到 React,最大的挑战往往不是 JSX 语法,而是对"渲染机制"底层逻辑的理解偏差。今天从一个看似简单的 Bug,探讨 React 的 Reconciliation(协调)算法原理。
前排广告位 :欢迎访问我的个人网站:https://hixiaohezi.com


在 React 社区的问答中,有一个现象非常普遍:"为什么我的 Input 输入框每输入一个字符就会失去焦点?"

其实是因为它触及了 React 最核心的渲染原理------组件身份(Identity)与协调(Reconciliation)。

问题背景

很多从 Vue 转向 React 的开发者(包括我自己),在初期为了"方便"或者实现逻辑闭环,可能会写出类似这样的代码:

jsx 复制代码
export default function UserForm() {
    const [name, setName] = useState('');

    // 🔴 错误示范:在组件内部定义组件
    // 很多开发者习惯在 Vue 的 template 中直接写局部逻辑,
    // 在 React 中容易误以为这样是在拆分 render 函数
    const StyledInput = ({ value, onChange }) => (
        <input 
            style={{ border: '1px solid blue', padding: '10px' }}
            value={value}
            onChange={onChange}
        />
    );

    return (
        <div>
            <h3>用户录入</h3>
            <StyledInput 
                value={name} 
                onChange={(e) => setName(e.target.value)} 
            />
            <p>当前输入:{name}</p>
        </div>
    );
}

Bug 出现:

当在输入框中按下第一个字符(比如 'a'),name 状态更新,组件重渲染。紧接着,输入框的焦点瞬间丢失。除了失去焦点,如果这个子组件里有任何内部 State,它们也会全部重置。

原理深度分析

要理解这个问题,必须深入 React 的 Diff 算法(协调过程)。React 在更新时,会比较新旧两棵虚拟 DOM 树(Fiber Tree)。

比较的一个核心策略是:类型(Type)的同一性检查。

1. 不同的"类"生成不同的树

React 检查一个节点时,首先看它的 type:

  • 如果 oldNode.type === newNode.type:React 认为这是同一个组件,它会保留该组件实例(及其 State),只更新 Props(Update)。
  • 如果 oldNode.type !== newNode.type:React 认为这是两个完全不同的东西,它会**卸载(Unmount)旧组件,丢失其所有状态和 DOM 节点,然后挂载(Mount)**新组件。

2. 函数也是对象

回到上面的代码。StyledInput 是在 UserForm 内部定义的函数。

这意味着:UserForm 每次渲染时,都会创建一个全新的 StyledInput 函数(引用地址不同)。

渲染流程回放:

  1. 第一次渲染:

    • 创建 StyledInput (函数地址 A)。
    • 渲染 <StyledInput /> (Type 是 A)。
    • 结果:挂载组件 A。
  2. 用户输入 'a' -> 触发 setName -> 触发第二次渲染:

    • UserForm 重新执行。
    • 创建新 的 StyledInput (函数地址 B)。注意:虽然代码逻辑一样,但在内存中 A !== B。
    • React 比较:旧节点的 Type 是 A,新节点的 Type 是 B。
    • 判定:Type 不同!这不是同一个组件!
    • 执行:卸载 A(移除 DOM,销毁 State),挂载 B(创建新 DOM,初始化 State)。

3.DOM 的毁灭与重生

因为组件被卸载并重新挂载,旧的 <input> DOM 元素被从页面中移除,一个新的 <input> 被创建并插入。
物理 DOM 都换了,焦点自然就没了。

Vue 与 React 的视角差异

对于习惯 Vue 的开发者来说,这种行为可能略显反直觉:

  • Vue 的视角 :组件通常是在 .vue 文件中静态定义的,或者在 components 选项中注册。组件定义的"引用"在整个应用生命周期中通常是稳定的(Static)。即使在 setup() 内部返回渲染函数,也不太容易犯"在渲染过程中重新定义组件类"的错误。
  • React 的视角:React Component 本质就是 JavaScript 函数。在 JS 中,函数内定义函数,等于每次执行外层函数时都新建一个闭包函数。React 的渲染完全依赖于 JS 的运行结果,因此对引用的稳定性要求极高。

解决方案

修正方案非常简单:保证组件类型(Type)的引用稳定性。

方案一:移到外部(标准解法)

将子组件定义移到父组件外面。这样 StyledInput 就在模块加载时被创建一次,永远不会变。

jsx 复制代码
// ✅ 正确:定义在外部,引用地址恒定
const StyledInput = ({ value, onChange }) => (
    <input ... />
);

export default function UserForm() {
    // ...
    return <StyledInput ... />;
}

方案二:使用 render props 或直接写 JSX

如果不仅仅是为了复用,只是为了拆分 JSX 结构,可以直接拆分成函数返回(注意不是组件,是返回 Element 的函数),或者直接把 JSX 存在变量里。

总结

在 React 中,组件在 UI 树中的位置(Position)和类型(Type)共同决定了它的身份(Identity)。身份变了,状态就没了。


欢迎访问我的个人网站:

👉 hixiaohezi.com

愿我们对原理的每一次深究,都能化作代码中的一份从容。

相关推荐
jktantan7 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
总要学点什么8 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
2601_962885728 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
excel9 小时前
encodeURI 和 encodeURIComponent 有什么区别?
前端
前端精髓9 小时前
调用openrouter的免费API模型
java·前端·javascript
赛博守夜人9 小时前
AI安全攻防与治理之十四:AI 安全测试不能脱离传统安全,Web、API、移动端与模型攻击面的组合拳
前端·人工智能·安全
开开心心_Every9 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
networknomad9 小时前
OpenReplay:自己搭的会话回放工具,给前端排错用的
前端·其他
缓慢更新10 小时前
AI 辅助生成单测:从接口定义到用例的流水线
java·服务器·前端·网络·数据库·人工智能
陈随易11 小时前
AI 运维服务器,安全又可靠的方法
前端·后端·程序员