React复习:基础组件+组件通信

191 阅读48分钟

声明:GLM4.5生成,辅助复习

好的!咱们用最接地气的大白话,把 React 的组件基础这个“地基”给夯得结结实实!想象一下,React 就像一套超级好玩的乐高积木,而组件就是这套积木里最基本的零件块。


🧩 一、组件是什么?—— 乐高积木块!

  • 大白话: 组件就是一个独立、可复用的小零件!就像乐高积木里的一块砖、一个轮子、一个窗户。你可以用这些小零件拼出大汽车、大房子(整个应用)。
  • 为什么重要? React 的核心思想就是**“万物皆组件”**!把复杂的页面拆成一个个功能明确的小组件,这样:
    • 好维护: 哪块砖坏了,换掉那块就行,不用拆整栋楼。
    • 好复用: 一个轮子组件,可以在汽车上用,可以在飞机上用,不用每次都重新造轮子。
    • 好理解: 看到一堆小零件组合,比看一坨乱麻的代码清晰多了。
  • 两种写法(现在主流是第一种):
    1. 函数组件 (最常用!像普通员工):
      function Welcome(props) {
        return <h1>你好, {props.name}!</h1>; // 返回要显示的内容
      }
      
      • 大白话: 就是一个普通的 JavaScript 函数!它接收一个叫 props 的“快递包裹”(后面讲),然后返回一段用 JSX(后面讲)写的“界面代码”。简单、直接、现在最流行!
    2. 类组件 (像部门经理,带状态和生命周期):
      class Welcome extends React.Component {
        render() {
          return <h1>你好, {this.props.name}!</h1>;
        }
      }
      
      • 大白话: 是一个 ES6 的类!它必须继承 React.Component,并且必须有一个 render() 方法,这个方法返回 JSX 写的界面。类组件有自己的“小金库”(state)和“人生阶段”(生命周期),但现在函数组件 + Hooks 也能做到,而且更简单,所以新项目基本都用函数组件了。

✍️ 二、JSX 是什么?—— 给 JavaScript 加了“画皮”!

  • 大白话: JSX 看起来像 HTML,但它不是真正的 HTML!它是一种 JavaScript 的语法扩展,让你能在 JS 代码里直接写“类似 HTML”的标签。React 会把它翻译成真正的 JavaScript 对象(虚拟 DOM),最后再变成浏览器能看的 HTML。
  • 为什么重要? 让写界面变得超级直观!不用再像以前那样用一堆 JS 命令去拼凑 HTML 字符串了。
  • 关键点(新手容易踩坑):
    1. {} 包 JS 表达式: 在 JSX 里想写 JS 变量、计算、函数调用,必须用花括号 {} 包起来!
      const name = "小明";
      const element = <h1>你好, {name}!</h1>; // 正确!
      // const element = <h1>你好, name!</h1>; // 错误!会显示字符串 "name"
      
    2. className 代替 class 因为 class 是 JavaScript 的关键字(用来定义类),所以 JSX 里写样式类名要用 className
      // 错误!
      // <div class="container">内容</div>
      // 正确!
      <div className="container">内容</div>
      
    3. 标签必须闭合:<img>, <br> 这种 HTML 里可以不闭合的标签,在 JSX 里必须写成自闭合形式 <img />, <br />
    4. 根元素只能有一个: 一个组件的 JSX 返回,最外层必须有且只有一个父标签。如果不想多一个无意义的 div,可以用 <></> (Fragment) 包起来。
      // 错误!
      // return <h1>标题</h1><p>段落</p>;
      // 正确!用 div 包
      return (
        <div>
          <h1>标题</h1>
          <p>段落</p>
        </div>
      );
      // 正确!用 Fragment 包(推荐,不会在DOM里多加div)
      return (
        <>
          <h1>标题</h1>
          <p>段落</p>
        </>
      );
      

📦 三、props (属性) —— 爸爸给儿子的“快递包裹”

  • 大白话: props (properties 的缩写) 是从父组件传递给子组件的数据!就像爸爸(父组件)给儿子(子组件)寄一个快递包裹,里面装着儿子需要的东西(数据)。子组件只能读,不能改!
  • 为什么重要? 这是组件间通信最基本、最常用的方式!让组件变得可配置可复用。同一个按钮组件,通过不同的 props(文字、颜色、大小),可以变成不同的按钮。
  • 怎么用?
    1. 父组件传值: 在子组件标签上写属性名=值,就像给快递贴单子。
      function App() {
        return <Welcome name="小红" age={18} />; // 传字符串用引号,传数字用{}
      }
      
    2. 子组件接收: 函数组件通过参数接收(通常叫 props),类组件通过 this.props 接收。
      // 函数组件接收
      function Welcome(props) {
        return (
          <div>
            <p>名字: {props.name}</p>
            <p>年龄: {props.age}</p>
          </div>
        );
      }
      
      // 类组件接收
      class Welcome extends React.Component {
        render() {
          return (
            <div>
              <p>名字: {this.props.name}</p>
              <p>年龄: {this.props.age}</p>
            </div>
          );
        }
      }
      
  • 核心规则:只读! 子组件拿到 props 后,绝对不能直接修改它!就像儿子不能拆爸爸寄来的包裹再寄回去(除非爸爸允许)。想改变数据?得让爸爸(父组件)自己改,然后重新传新的包裹过来。

💰 四、state (状态) —— 组件自己的“小金库”

  • 大白话: state组件自己内部管理的数据!就像组件自己口袋里的钱,想怎么花(改)就怎么花(改)。state 一改变,组件就会自动重新渲染(刷新界面)!
  • 为什么重要? 这是让组件“活起来”的关键!比如按钮的点击次数、输入框的内容、开关的开关状态、列表的数据... 这些会变化的东西,都应该放在 state 里。
  • 怎么用?
    1. 函数组件:用 useState Hook (最常用!)
      import React, { useState } from 'react'; // 1. 引入
      
      function Counter() {
        // 2. 声明一个 state 变量 `count`,初始值是 0
        //    setCount 是用来更新 count 的函数
        const [count, setCount] = useState(0);
      
        return (
          <div>
            <p>你点了 {count} 次</p>
            {/* 3. 点击按钮时,调用 setCount 更新 state */}
            <button onClick={() => setCount(count + 1)}>
              点我 +1
            </button>
          </div>
        );
      }
      
      • 关键点:
        • useState(初始值) 返回一个数组:[当前值, 更新函数]
        • 必须用 setCount 来更新! 直接改 count = count + 1 无效!React 感知不到。
        • 更新是异步的: React 可能会合并多次更新,不要依赖 setCount 后立刻拿到新值。
        • 不可变数据: 更新对象或数组时,要创建新对象/新数组,不要直接改原对象/原数组!(比如 setUser({...user, age: 20}) 而不是 user.age = 20
    2. 类组件:用 this.statethis.setState()
      class Counter extends React.Component {
        constructor(props) {
          super(props);
          // 1. 在构造函数里初始化 state
          this.state = {
            count: 0
          };
        }
      
        render() {
          return (
            <div>
              <p>你点了 {this.state.count} 次</p>
              {/* 2. 调用 this.setState 更新 state */}
              <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                点我 +1
              </button>
            </div>
          );
        }
      }
      
      • 关键点:
        • constructor 里用 this.state = { ... } 初始化。
        • 必须用 this.setState() 更新! 直接改 this.state.count 无效且是错误实践!
        • this.setState 可以接受对象({ count: newCount })或函数((prevState, props) => ({ count: prevState.count + 1 })),后者更安全(避免依赖旧 state)。
        • 合并更新: this.setState 会自动合并你传入的对象到当前 state 中(只更新你指定的属性)。

🖱️ 五、事件处理 —— 给组件装上“遥控器”

  • 大白话: 就是处理用户在界面上做的操作,比如点击按钮、输入文字、鼠标移入移出等。React 给这些操作绑定了处理函数(“遥控器按键”)。
  • 关键点:
    1. 命名: React 事件名用驼峰命名法onClick, onChange, onMouseOver),而不是 HTML 的小写(onclick, onchange)。
    2. 绑定函数: 事件处理函数通常用 {} 包裹一个函数。
      // 函数组件
      function Button() {
        const handleClick = () => {
          alert('按钮被点了!');
        };
      
        return <button onClick={handleClick}>点我</button>;
      }
      
      // 类组件
      class Button extends React.Component {
        handleClick() {
          alert('按钮被点了!');
        }
      
        render() {
          // 注意:这里需要用 this.handleClick,并且通常需要绑定 this (见下)
          return <button onClick={this.handleClick}>点我</button>;
        }
      }
      
    3. 类组件的 this 问题(坑!): 在类组件里,事件处理函数里的 this 默认是 undefined,不是组件实例!需要绑定 this
      • 方法1:构造函数里绑定 (推荐)
        class Button extends React.Component {
          constructor(props) {
            super(props);
            this.handleClick = this.handleClick.bind(this); // 绑定!
          }
        
          handleClick() {
            console.log(this); // 现在是 Button 组件实例了
          }
          // ... render
        }
        
      • 方法2:使用箭头函数 (类属性语法)
        class Button extends React.Component {
          handleClick = () => { // 箭头函数自动绑定 this
            console.log(this); // 是 Button 组件实例
          }
          // ... render
        }
        
      • 方法3:在 JSX 里用箭头函数 (不推荐,每次渲染都创建新函数,可能影响性能)
        render() {
          return <button onClick={() => this.handleClick()}>点我</button>;
        }
        
    4. 传递参数: 想在事件处理函数里传额外参数?
      • 函数组件: 用箭头函数包裹。
        function DeleteButton({ id }) {
          const handleDelete = (idToDelete) => {
            console.log(`要删除 ID 为 ${idToDelete} 的项目`);
          };
        
          return (
            <button onClick={() => handleDelete(id)}>
              删除
            </button>
          );
        }
        
      • 类组件: 同样用箭头函数包裹,或者用 .bind(this, 参数)
        class DeleteButton extends React.Component {
          handleDelete(idToDelete) {
            console.log(`要删除 ID 为 ${idToDelete} 的项目`);
          }
        
          render() {
            return (
              <button onClick={() => this.handleDelete(this.props.id)}>
                删除
              </button>
            );
          }
        }
        

🔄 六、条件渲染 —— 让组件“会变脸”

  • 大白话: 根据不同的情况(比如 state 的值、props 的值),让组件显示不同的内容或完全不显示。就像一个演员,根据剧本演不同的角色。
  • 常用方法:
    1. if 语句: 最直接,在 return 之前判断。
      function Greeting({ isLoggedIn }) {
        if (isLoggedIn) {
          return <h1>欢迎回来!</h1>;
        } else {
          return <h1>请先登录。</h1>;
        }
      }
      
    2. 三元运算符 ? : 简洁,适合在 JSX 里直接写。
      function Greeting({ isLoggedIn }) {
        return (
          <div>
            {isLoggedIn ? (
              <h1>欢迎回来!</h1>
            ) : (
              <h1>请先登录。</h1>
            )}
          </div>
        );
      }
      
    3. 逻辑与 && 当条件为 true 时才渲染后面的内容(常用于显示/隐藏)。
      function Notification({ unreadCount }) {
        return (
          <div>
            <h1>通知中心</h1>
            {unreadCount > 0 && (
              <span className="badge">{unreadCount}</span>
            )}
          </div>
        );
      }
      // 如果 unreadCount 是 0,<span> 就不会渲染
      

📋 七、列表渲染 —— 批量生产“零件”

  • 大白话: 当你有一组数据(比如数组),需要根据这组数据生成一组相同的组件结构(比如列表项)时用的。就像工厂流水线,根据图纸(数据)生产出一堆一模一样的零件(列表项)。
  • 核心方法:用数组的 .map() 方法
    function NumberList({ numbers }) {
      return (
        <ul>
          {numbers.map((number) => (
            <li key={number.toString()}>{number}</li>
          ))}
        </ul>
      );
    }
    
    // 使用
    const numbers = [1, 2, 3, 4, 5];
    <NumberList numbers={numbers} />
    
  • 超级重要的 key 属性!
    • 为什么必须? React 需要一个唯一且稳定的标识(key)来区分列表中的每一项。当列表数据变化(增删改排序)时,React 靠 key 高效地找出哪些项是新的、哪些项被移动了、哪些项被删除了,从而只更新必要的部分(虚拟 DOM Diff 算法的基础)。
    • 怎么选 key
      • 最佳选择: 数据项唯一且稳定的 ID(比如数据库 id)。key={item.id}
      • 次优选择: 如果没有 id,可以用索引 indexkey={index}但!只适用于列表顺序永远不会改变**(纯展示,不会增删排序)的情况!** 因为索引会随着列表变化而改变,可能导致渲染问题或性能下降。
    • key 的坑:
      • key 是给 React 内部用的,不要在组件内部通过 props.key 访问它!
      • key 必须在兄弟节点之间是唯一的(同一个 <ul> 里的所有 <li>key 不能重复)。
      • 不要用随机数 Math.random() 或时间戳 Date.now()key!它们每次渲染都会变,失去意义,反而会害了 React!

🏁 总结:组件基础 = 乐高积木玩法手册

  1. 组件是零件: 函数组件(主流)或类组件。
  2. JSX 是画皮: 在 JS 里写类 HTML,记得 {} 包 JS,className 代替 class,标签要闭合,根元素唯一。
  3. props 是快递: 父传子,子只读不可改,让组件可配置复用。
  4. state 是小金库: 组件自己的数据,改了就刷新界面(函数组件用 useState,类组件用 this.state + this.setState)。
  5. 事件是遥控器: 处理用户操作(驼峰命名,类组件注意 this 绑定)。
  6. 条件渲染会变脸: if / ? : / && 根据情况显示不同内容。
  7. 列表渲染靠 .map() 批量生成,必须加唯一稳定的 key(优先用 id,慎用 index)。

记住这个核心:React 组件就是接收 props(输入),管理 state(内部状态),返回 JSX(输出)的小机器! 把这几个基础概念和用法吃透,你就掌握了 React 80% 的日常开发!剩下的就是不断练习组合这些“零件”啦!💪

  • 使用简洁的大白话讲解React 事件机制,React的事件和普通的HTML事件有什么不同,React的事件和普通的HTML事件有什么不同,React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代,对React-Fiber的理解,它解决了什么问题,React.Component 和 React.PureComponent 的区别,Component, Element, Instance 之间有什么区别和联系,React.createClass和extends Component的区别有哪些 好的!咱们用最简洁的大白话,把这些 React 核心概念串起来讲清楚,像剥洋葱一样一层层看明白。

🎯 1. React 事件机制 vs 普通 HTML 事件

  • 普通 HTML 事件:

    • 大白话: 直接在 DOM 元素上绑事件(比如 onclick="handleClick()")。浏览器自己负责监听和触发。每个元素都可能有独立的事件处理器,如果元素很多(比如列表里 1000 个 li 都绑 click),就会占用很多内存,性能可能变差。
    • 特点: 原生、直接、分散。
  • React 事件:

    • 大白话: React 搞了个“事件代理”(也叫“合成事件”)。它不在每个元素上绑事件,而是在最外层的容器(比如 div#root)上绑一个总事件监听器。当你在 JSX 里写 onClick={...} 时,React 并不是真的给那个元素绑事件,而是记录下“这个元素被点了,要执行这个函数”。当事件真的发生时(比如你点了个按钮),事件会冒泡到最外层容器,被那个总监听器抓住。然后 React 根据事件发生的目标元素,找到你之前记录的对应函数去执行。
    • 关键不同点:
      1. 事件委托: 只在顶层绑一个监听器,性能更好(尤其列表多时)。
      2. 合成事件(SyntheticEvent): React 把原生浏览器事件包装了一下,做了一个跨浏览器兼容的“假事件对象”(SyntheticEvent)。你拿到的 e 不是原生的,但用法差不多(e.preventDefault(), e.stopPropagation() 都能照用),并且 React 保证它在所有浏览器里行为一致。
      3. 事件池(旧版): 以前 React 为了性能,会复用事件对象。事件处理函数执行完,事件对象里的属性会被清空。所以你不能异步访问它(比如 setTimeout 里用 e)。新版 React (v17+) 取消了事件池,这个问题没了,e 可以放心异步用。
      4. 命名: React 用驼峰命名(onClick, onChange),HTML 用小写(onclick, onchange)。

🔧 2. 高阶组件 (HOC) vs Render Props vs Hooks (代码复用三兄弟)

  • 共同目标: 解决组件逻辑复用的问题(比如多个组件都需要获取数据、监听窗口大小、处理表单等)。

  • 高阶组件 (HOC - Higher-Order Component):

    • 大白话: 就像给组件“穿衣服”或“加装备”。它是一个函数,接收一个组件(WrappedComponent)作为参数,返回一个增强后的新组件。新组件包裹了原组件,并注入了额外的功能(propsstate)。
    • 样子: const EnhancedComponent = withHOC(OriginalComponent);
    • 例子: React-Reduxconnect 就是个 HOC,它给组件注入了 statedispatch
    • 缺点: 容易造成“嵌套地狱”(withA(withB(withC(Component)))),调试时组件层级深;props 命名可能冲突;this 指向问题(类组件时代)。
  • Render Props:

    • 大白话: 组件不自己渲染内容,而是提供一个“渲染函数”作为 prop,让调用者(父组件)决定具体渲染什么。这个“渲染函数”会接收组件内部的数据或状态作为参数。
    • 样子: <DataProvider render={(data) => <Child data={data} />} /> 或者 <DataProvider>{(data) => <Child data={data} />}</DataProvider>
    • 例子: React Router v6 的 OutletuseOutletContext 有点类似思想;很多动画库、数据获取库用过。
    • 优点: 比 HOC 更灵活,避免了嵌套地狱和命名冲突。
    • 缺点: 写法稍显“奇怪”,可能产生深层嵌套的回调(虽然比 HOC 好点);性能上如果 render 函数每次都是新创建的,可能导致子组件不必要的重渲染(需要 React.memo 配合)。
  • Hooks:

    • 大白话: 革命性方案! 让你在不写 class 的情况下,也能“钩住”(Hook) React 的状态(useState)、生命周期(useEffect)、上下文(useContext)等特性。逻辑复用通过自定义 Hook 实现(把共享逻辑抽到一个函数里,里面用各种内置 Hook)。
    • 样子: const [data, setData] = useCustomHook(); (自定义 Hook)
    • 优点:
      • 彻底解决嵌套问题: 逻辑是平铺的函数调用,不再有 HOC 的包裹地狱或 Render Props 的回调嵌套。
      • 更直观: 逻辑和 UI 更紧密地写在函数组件里,符合直觉。
      • 组合更灵活: 自定义 Hook 可以自由组合,像搭积木一样。
      • 避免 this 问题: 函数组件没有 this
    • 为什么是迭代? Hooks 是 React 团队吸取 HOC 和 Render Props 的经验教训后,找到的最优解,是当前及未来的主流范式

⏳ 3. 为什么要不断迭代?(React 的进化史)

  • 大白话: 为了解决痛点拥抱新标准提升性能改善开发者体验
    • createClass -> extends Component 为了拥抱 ES6 class 语法(更标准、更强大),摆脱 React 自己造的“类”语法糖。
    • Mixin -> HOC -> Render Props -> Hooks: 为了更好地复用组件逻辑。Mixin 早期方案,问题多(命名冲突、依赖不透明);HOC 和 Render Props 是进步,但有各自缺点;Hooks 是目前最优雅、最强大的解决方案。
    • Stack Reconciler -> Fiber Reconciler: 为了解决性能瓶颈(主线程阻塞导致的卡顿),实现可中断渲染优先级调度(Fiber)。
    • 旧版生命周期 -> 新版生命周期 + Hooks: 为了消除隐患(如 componentWillMount/componentWillUpdate 在异步渲染下可能不安全),并让逻辑组织更清晰(Hooks 将相关逻辑聚合在一起)。
    • 事件池取消: 为了消除陷阱(异步访问事件对象),简化开发。
    • Suspense(实验性/逐步稳定): 为了优雅处理异步操作(数据获取、代码分割),让“加载中”状态声明式管理。

核心驱动力: 让 React 更快(性能)、更强(能力)、更好用(开发者体验)、更健壮(稳定性)。


🌿 4. React Fiber 理解 (React 的“心脏手术”)

  • 大白话: Fiber 是 React 底层协调算法(Reconciliation)的重写,是 React 16 的核心引擎升级。你可以把它想象成给 React 做了一次“心脏搭桥手术”,让它能处理更复杂的任务而不会“心肌梗塞”(页面卡顿)。

  • 解决了什么问题?

    • 旧问题(Stack Reconciler): React 更新(比如状态变化导致重新渲染)是一口气做完的,而且是同步的。如果更新任务很重(比如渲染一个超长列表),它会霸占主线程,直到全部完成。在这期间,浏览器啥也干不了(无法响应用户输入、动画等),页面就卡住了(掉帧)。
    • Fiber 的解决方案:
      1. 可中断渲染: Fiber 把更新任务拆分成很多小单元(每个 Fiber 节点代表一个工作单元)。React 可以开始一个任务,暂停它,去做更重要的事(比如用户输入),然后再回来继续。就像你写报告,中间可以接个电话再回来写。
      2. 优先级调度: React 能区分任务的紧急程度。用户输入、动画这种需要立即响应的任务,优先级最高;后台数据更新这种不那么急的,优先级低。高优先级任务可以打断低优先级任务。
      3. 增量更新: 更新可以分批完成,每次只处理一部分,然后把控制权交还给浏览器,让它去处理绘制、响应用户等,避免长时间阻塞。
      4. 支持并发模式(Concurrent Mode): 这是 Fiber 带来的未来方向。让 React 能同时准备多套 UI。比如,新数据来了,React 可以在后台悄悄渲染新界面,等准备好了再无缝切换过去,用户感觉不到“加载中”。或者,在用户快速切换标签时,React 可以放弃未完成的不重要渲染,避免浪费资源。
  • 对开发者的影响: 大部分时候你感觉不到 Fiber 的存在(API 基本没变),但你的应用在处理复杂交互和大量数据时,会明显更流畅。理解 Fiber 有助于你写出性能更好的代码(比如避免在渲染路径上做太重的同步计算)。


⚖️ 5. React.Component vs React.PureComponent

  • 大白话: 两者都是类组件的基类。PureComponentComponent 的一个“精打细算版”。

  • 核心区别:shouldComponentUpdate 的实现

    • React.Component 默认情况下,只要父组件重渲染,或者组件自己的 setState 被调用,它就一定会重渲染shouldComponentUpdate 默认返回 true)。它不管 propsstate 到底变没变。
    • React.PureComponent自动实现了 shouldComponentUpdate!在重渲染前,它会浅比较(Shallow Compare) 当前组件的 propsstate 与上一次的 propsstate
      • 如果 propsstate(浅层)都没变不重渲染!(shouldComponentUpdate 返回 false)
      • 如果 propsstate(浅层)有变重渲染!(shouldComponentUpdate 返回 true)
  • 浅比较 (Shallow Compare) 是啥?

    • 大白话: 只比较第一层。对于基本类型(数字、字符串、布尔值),直接比值是否相等。对于对象/数组,只比引用地址是否相同(是不是同一个对象),不会深入比较对象内部属性是否变了
      // 浅比较结果
      const a = 1;
      const b = 1; // a === b -> true (基本类型比值)
      
      const obj1 = { name: '张三' };
      const obj2 = { name: '张三' }; // obj1 === obj2 -> false (对象比引用地址)
      
      const obj3 = obj1; // obj3 === obj1 -> true
      
  • PureComponent 的坑(使用注意):

    • 确保 stateprops 不可变! 如果你直接修改了 stateprops 里的对象/数组(比如 this.state.user.name = '李四'),因为引用地址没变,PureComponent 会认为没变化,导致不更新 UI(Bug)!正确做法是创建新对象/新数组this.setState({ user: { ...this.state.user, name: '李四' } }))。
    • 避免复杂结构: 如果 propsstate 结构很深,浅比较可能失效(内部变了但引用没变),或者比较本身有性能开销。此时可能需要手动在 Component 里实现 shouldComponentUpdate 做深比较,或者用 React.memo(函数组件)配合自定义比较函数。
  • 总结: PureComponent 是一个性能优化工具,适用于 propsstate 结构简单、更新不频繁的场景。务必配合不可变数据使用! 函数组件对应的是 React.memo


📐 6. Component, Element, Instance 的区别与联系

  • 大白话: 想象盖房子:

    • Element (元素): 设计图纸。它是一个普通的 JavaScript 对象,描述了你想要在屏幕上看到什么(比如:一个按钮,文字是“点击我”,类型是 button)。它不是真实的 DOM 节点,只是轻量的描述React.createElement() 返回的就是 Element,JSX 最终也会被转成 Element。
    • Component (组件): 建造房子的工厂建筑师。它是一个函数或类,定义了如何根据输入(props)生成设计图纸(Element)。组件本身不直接出现在屏幕上,它负责创建和管理 Element。
    • Instance (实例): 盖好的具体房子。对于类组件,当 React 根据 Component 的“图纸”去“施工”(渲染)时,会创建一个该类的实例。这个实例持有组件的 state、生命周期方法等,是组件在运行时的具体体现函数组件没有实例(每次渲染都是一次函数调用)。
  • 联系与流程:

    1. 你写代码定义 Component(函数或类)。
    2. React 调用你的 Component(函数组件直接调用,类组件创建实例并调用 render 方法)。
    3. Component 执行后,返回一个或多个 Element(描述 UI 的对象)。
    4. React 拿到这些 Element,通过 Diff 算法 比较新旧 Element 树,找出需要更新的最小操作。
    5. React 将这些操作应用到真实的 DOM 上(渲染)。
    6. 对于类组件,在步骤 2 中创建的 Instance 会在组件的整个生命周期中存在,管理 state 和响应生命周期方法。函数组件没有这个持久化的 Instance。
  • 简单记忆:

    • Element = 图纸 (描述 UI 的 JS 对象)
    • Component = 工厂/建筑师 (创建图纸的函数/类)
    • Instance = 盖好的房子 (类组件运行时的具体对象,函数组件无)

🏗️ 7. React.createClass vs extends Component

  • 大白话: 这是 React 创建类组件的两种旧方式 vs 新方式。现在 extends Component 是标准,createClass 已被废弃(v15.5 后移到单独包,v16+ 彻底不用)。

  • 核心区别:

特性React.createClass (旧)extends Component (新)
语法基础React 自己实现的类语法糖标准 ES6 Class 语法
this 绑定自动绑定!方法里的 this 自动指向组件实例。需要手动绑定!常用方法:
1. 构造函数里 bind
2. 箭头函数(类属性)
3. JSX 里箭头函数(不推荐)
状态初始化getInitialState() 方法构造函数里 this.state = { ... }
props 默认值getDefaultProps() 方法类静态属性 static defaultProps = { ... }
propTypes对象属性 propTypes: { ... }类静态属性 static propTypes = { ... }
Mixins支持mixins: [SomeMixin] (曾是复用逻辑方式)不支持!被 HOC / Render Props / Hooks 取代
兼容性废弃,需要额外引入包标准,现代 React 唯一方式
  • 为什么淘汰 createClass

    1. 拥抱标准: ES6 Class 是 JavaScript 的官方标准,更通用,学习成本更低(学 JS 就会)。
    2. this 绑定清晰: 虽然 createClass 自动绑定方便,但隐藏了 this 机制。ES6 Class 要求手动绑定,让开发者更清楚 this 的指向问题(虽然麻烦点,但更符合 JS 原生行为)。
    3. Mixins 的弊端: Mixins 是早期复用逻辑的方式,但容易造成命名冲突、依赖不透明、难以维护。React 团队明确转向组合模式(HOC, Render Props, Hooks)。
    4. 简化 API: 去除 React 自创的 API(getInitialState, getDefaultProps),统一到 ES6 Class 的标准写法上。
  • 总结: extends Component现代、标准、推荐的类组件写法。createClass历史遗迹,了解其存在和区别即可,新项目绝对不要用。


🎯 总复习口诀

  • 事件: React 搞代理,顶层抓事件,合成跨浏览器。
  • 复用: HOC 穿衣服,Render Props 给画笔,Hooks 是万能瑞士军刀(迭代最优解)。
  • 迭代: 为解痛点、跟标准、提性能、优体验。
  • Fiber: 可中断、分优先、增量更、并发未来(解决卡顿)。
  • PureComponent: 浅比较 props/state,省渲染,但需 immutable 配合。
  • 三兄弟: Element 是图纸,Component 是工厂,Instance 是类组件盖好的房(函数组件无房)。
  • createClass vs Component: 旧糖 vs 新标,自动绑 vs 手绑,Mixin 死了,Component 活着!

把这些概念的核心区别和联系记牢,React 的底层逻辑和设计思想就清晰了一大半!加油!💪

  • 使用简洁的大白话讲解React 高阶组件是什么,和普通组件有什么区别,适用什么场景,对componentWillReceiveProps 的理解,哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?React如何判断什么时候重新渲染组件?React声明组件有哪几种方法,有什么不同?对有状态组件和无状态组件的理解及使用场景,对React中Fragment的理解,它的使用场景是什么?React如何获取组件对应的DOM元素?React中可以在render访问refs吗?为什么?对React的插槽(Portals)的理解,如何使用,有哪些使用场景?在React中如何避免不必要的render?

好的!咱们用最简洁的大白话,把这些问题一次性盘明白,像吃火锅一样,一个个涮清楚!


🧩 1. React 高阶组件 (HOC) 是什么?和普通组件区别?适用场景?

  • HOC 是什么?

    • 大白话: HOC 是一个函数,接收一个组件作为参数,返回一个增强后的新组件。就像给组件“穿功能外套”或“加装插件”。
    • 样子: const EnhancedComponent = withHOC(OriginalComponent);
  • 和普通组件的区别?

    • 普通组件: 直接渲染 UI,接收 props,可能管理 state。是“成品”。
    • HOC: 不是组件本身,是制造组件的函数。它不渲染 UI,它的产物(返回的新组件)才渲染 UI。是“组件加工厂”。
  • 适用场景?

    • 代码复用: 把多个组件需要的相同逻辑抽出来(如:权限校验、数据获取、日志记录、路由守卫)。
    • 属性劫持/注入: 给被包裹组件添加/修改 props(如:React-Reduxconnect 注入 statedispatch)。
    • 渲染劫持: 控制被包裹组件的渲染过程(如:条件渲染、包裹额外 UI)。
    • 注意: 现在更推荐用 Hooks 解决逻辑复用,HOC 容易造成“嵌套地狱”。

🔄 2. 对 componentWillReceiveProps 的理解?

  • 大白话: 这是类组件的一个旧生命周期方法。当组件接收到新的 props 时(注意:首次渲染时不触发),在渲染之前调用。让你有机会根据新的 props 去更新组件的 state
  • 关键点:
    • 触发时机: 父组件重传 props 导致子组件更新时(不是自己 setState 触发)。
    • 作用: 比较 nextPropsthis.props,如果需要,用 this.setState() 更新内部状态。
    • ⚠️ 已废弃! 因为在 React 16.3+ 的异步渲染机制(Fiber) 下,它可能被多次调用或打断,导致状态更新不可靠或性能问题。
  • 替代方案:
    • static getDerivedStateFromProps(nextProps, prevState) 静态方法,在每次渲染前(包括首次)都调用。根据 props 计算并返回新的 state 对象(或 null 表示不更新)。纯函数,无副作用
    • componentDidUpdate(prevProps, prevState)渲染后调用。可以在这里执行副作用(如网络请求),并根据 prevPropsthis.props 的差异更新 state(但要注意避免无限循环)。

🚀 3. 哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

  • 触发重新渲染的方法:

    1. this.setState() (类组件): 最常用! 改变组件内部状态。
    2. this.forceUpdate() (类组件): 强制重渲染跳过 shouldComponentUpdate 检查,直接调用 render慎用! 通常意味着设计有问题。
    3. 父组件重渲染: 父组件 render 了,会重新渲染所有子组件(除非子组件做了优化,如 PureComponent / React.memo)。
    4. useStatesetter 函数 (函数组件): 改变状态,触发函数组件重新执行。
    5. useReducerdispatch (函数组件): 触发状态更新,导致重渲染。
    6. Context 的 Provider value 变化: 会导致所有消费该 Context 的组件重渲染(除非用 React.memo 优化)。
  • 重新渲染 render 会做些什么?

    • 大白话: render 方法(函数组件就是整个函数体)被调用,生成新的 React 元素(虚拟 DOM 树)
    • 具体步骤:
      1. 调用 render() 执行组件的 render 方法(类组件)或整个函数组件(函数组件)。
      2. 生成新 Element 树: render 返回一个描述 UI 结构的 React Element 对象树(虚拟 DOM)。
      3. Diff 算法比较: React 拿着新 Element 树上一次渲染的 Element 树进行对比(Diffing)。
      4. 计算最小更新: 找出两棵树之间最少的 DOM 操作(哪些节点需要添加、删除、更新属性)。
      5. 提交更新 (Commit): 将计算出的 DOM 操作应用到真实浏览器 DOM 上,完成界面更新。

🤔 4. React 如何判断什么时候重新渲染组件?

  • 核心原则:单向数据流 + 状态驱动。
  • 判断依据:
    1. 组件自身的 state 改变了: 通过 setState (类) 或 useState/useReducer (函数) 触发。
    2. 组件接收的 props 改变了: 父组件重渲染导致传下来的 props 变了(浅比较)。
    3. 父组件重渲染了: 即使子组件 props 没变,父组件 render 了,默认也会导致子组件重渲染(除非子组件做了优化)。
    4. 强制更新: 调用了 forceUpdate() (类组件)。
  • 优化点(避免不必要的渲染):
    • 类组件: 继承 PureComponent(自动浅比较 props/state)或手动实现 shouldComponentUpdate(返回 false 阻止渲染)。
    • 函数组件:React.memo 包裹(自动浅比较 props)或结合 useMemo/useCallback 优化传递给子组件的 props

🏗️ 5. React 声明组件有哪几种方法?有什么不同?

方法类型语法状态管理生命周期this适用场景备注
函数组件函数function MyComp() {...}Hooks (useState)Hooks (useEffect)主流! 简单 UI、逻辑复用现代、简洁、推荐
类组件class MyComp extends Componentthis.state生命周期方法,需绑定复杂状态/逻辑、旧项目标准、强大
React.createClass函数(糖)React.createClass({...})getInitialState生命周期方法自动绑定已废弃! 历史项目旧 API,避免使用
  • 核心不同:
    • 函数组件: 更轻量、无 this、逻辑通过 Hooks 组织。当前及未来主流
    • 类组件: 更传统、有 this、生命周期方法清晰。适合复杂场景和旧代码。
    • createClass 过时产物,有自动绑定 this 等特性,但已被 ES6 Class 彻底取代。

🧊 6. 有状态组件 vs 无状态组件?理解及场景?

  • 有状态组件 (Stateful Component):

    • 大白话: 组件自己管理数据state)。它有“记忆”,知道当前的状态(比如按钮点了几次、输入框内容是什么),并且能根据用户交互或时间变化更新自己的状态
    • 特点: 通常使用类组件或带 useState/useReducer 的函数组件。
    • 使用场景:
      • 需要处理用户交互(表单、按钮点击)。
      • 需要根据数据变化更新 UI(计数器、定时器、动态列表)。
      • 需要管理复杂的内部逻辑和状态。
  • 无状态组件 (Stateless Component / Presentational Component):

    • 大白话: 组件不管理数据state)。它像个“傻显示器”,只负责展示从外面(props)传进来的数据。它没有“记忆”,给什么数据就显示什么,自己不会变。
    • 特点: 通常是纯函数组件(或只读 props 的类组件)。只依赖 props 进行渲染。
    • 使用场景:
      • 纯展示 UI(按钮、图标、卡片、列表项)。
      • 容器组件(Container)和展示组件(Presentational)分离模式中的展示层。
      • 提高可复用性和可测试性(输入输出明确)。
  • 趋势: 函数组件 + Hooks 让“无状态组件”也能轻松拥有状态(useState),界限变得模糊。但思想依然重要:尽量让组件专注于展示(无状态),复杂逻辑和状态提升到上层(有状态组件或自定义 Hook)。


🧩 7. 对 React 中 Fragment 的理解?使用场景?

  • 大白话: Fragment 是一个空的包裹标签 <></><React.Fragment></React.Fragment>。它允许你返回多个元素,但不会在最终 DOM 中添加任何实际的节点
  • 为什么需要? React 组件的 render 方法必须返回单个根元素。如果不想多包一个无意义的 <div>(可能破坏样式结构,如表格、列表),就用 Fragment。
  • 使用场景:
    1. 避免额外 DOM 节点:
      // 错误!需要根元素
      // return <td>Hello</td><td>World</td>;
      // 正确!用 Fragment 包裹,DOM 里只有两个 <td>
      return (
        <>
          <td>Hello</td>
          <td>World</td>
        </>
      );
      
    2. 表格、列表等严格结构:<table>, <ul>, <dl> 内部直接放 <div> 是非法的,Fragment 是完美解决方案。
    3. CSS 样式影响: 避免多余的 <div> 破坏 Flex/Grid 布局或选择器。
  • <></> vs <React.Fragment>
    • <></>简洁,但不能接受 key 属性。
    • <React.Fragment>稍长,但可以接受 key 属性(在渲染列表时有用)。

📍 8. React 如何获取组件对应的 DOM 元素?

  • 方法:使用 ref
    1. 创建 ref
      • 函数组件: const myRef = useRef(null);
      • 类组件: this.myRef = React.createRef(); (在构造函数) 或 myRef = React.createRef(); (类属性)
    2. 绑定到元素: <div ref={myRef}>...</div>
    3. 访问 DOM:
      • 函数组件: myRef.current (指向 DOM 元素)
      • 类组件: this.myRef.current (指向 DOM 元素)
  • 例子:
    // 函数组件
    import { useRef, useEffect } from 'react';
    
    function MyComponent() {
      const inputRef = useRef(null);
    
      useEffect(() => {
        // 组件挂载后,inputRef.current 指向 <input> DOM 元素
        inputRef.current.focus();
      }, []);
    
      return <input ref={inputRef} type="text" />;
    }
    

🚫 9. React 中可以在 render 访问 refs 吗?为什么?

  • 答案:不可以!
  • 为什么?
    • 大白话: render 阶段是 React 在**“画图纸”(生成虚拟 DOM),此时真实的 DOM 元素还没创建出来呢!refs 是用来指向真实 DOM** 的,你访问它只能得到 null。就像房子还在施工图阶段,你不可能找到房间的门把手。
    • 生命周期角度:
      • render 纯计算阶段,不能有副作用(访问 DOM、发网络请求等)。
      • componentDidMount (类) / useEffect (函数): 提交阶段后,此时 DOM 已创建并挂载refs.current 才会指向真实的 DOM 元素。访问 refs 的正确时机!
  • 错误示例:
    class BadComponent extends React.Component {
      inputRef = React.createRef();
    
      render() {
        // ❌ 错误!render 里访问 ref.current 是 null
        this.inputRef.current.focus(); // 报错!
        return <input ref={this.inputRef} />;
      }
    }
    

🌀 10. 对 React 的插槽 (Portals) 的理解?如何使用?场景?

  • 大白话: Portal 是一个“传送门”!它允许你把一个组件渲染到父组件 DOM 树之外的任意位置(比如 document.body),但该组件在 React 组件树中的位置和事件冒泡关系保持不变
  • 如何使用?
    1. 创建 Portal 容器:public/index.html 里加一个 <div id="portal-root"></div>
    2. 使用 ReactDOM.createPortal
      import { ReactDOM } from 'react-dom';
      
      function Modal({ children }) {
        // 将 children 渲染到 portal-root 这个 DOM 节点
        return ReactDOM.createPortal(
          children, // 要渲染的 React 元素
          document.getElementById('portal-root') // 目标 DOM 节点
        );
      }
      
      // 使用
      function App() {
        return (
          <div>
            <h1>普通内容</h1>
            <Modal>
              <div className="modal">
                <h2>我是 Modal!</h2>
                <p>虽然我渲染在 body 下,但事件冒泡会回到 App 组件</p>
              </div>
            </Modal>
          </div>
        );
      }
      
  • 关键特性: Portal 里的元素,虽然物理上在 #portal-root 里,但在 React 的虚拟 DOM 树中,它仍然是 App 组件的子组件。事件(如点击)会沿着 React 组件树冒泡,而不是 DOM 树。
  • 使用场景:
    1. 模态框 (Modal) / 对话框 (Dialog): 需要覆盖在所有内容之上,不受父容器 overflow: hiddenz-index 影响。渲染到 body 下最合适。
    2. 提示框 (Tooltip) / 弹出菜单 (Popups): 需要定位在特定元素附近,但可能被父容器的 overflow 裁剪。Portal 可以让它“逃逸”出来。
    3. 全局通知 (Notifications): 固定在屏幕角落,不受页面滚动影响。
    4. 第三方库集成: 需要将 React 组件渲染到非 React 管理的 DOM 区域。

🛡️ 11. 在 React 中如何避免不必要的 render?

  • 核心思路: 让 React 知道“即使父组件重渲染了,我的 propsstate 其实没变,不用重新渲染我”。
  • 具体方法:
    1. 函数组件:React.memo
      • 作用: 包裹函数组件,浅比较 props。如果 props 没变(浅层),跳过渲染。
      • 用法: const MemoizedComponent = React.memo(MyComponent);
      • 进阶: 可提供自定义比较函数 (arePropsEqual) 做深比较或特定逻辑。
    2. 类组件:PureComponentshouldComponentUpdate
      • PureComponent 继承它代替 Component。自动浅比较 this.propsthis.state,没变就不渲染。
      • shouldComponentUpdate(nextProps, nextState) 手动实现。返回 true(渲染)或 false(不渲染)。可自定义比较逻辑(如深比较特定字段)。
    3. 优化传递给子组件的 props (函数组件关键!)
      • 问题: 父组件每次渲染,如果直接传新函数 {() => doSomething()} 或新对象 { data: { ... } },即使内容一样,引用地址也变了,导致子组件(即使 React.memo 了)认为 props 变了而重渲染。
      • 解决方案:
        • useCallback 缓存函数。依赖项不变时,返回同一个函数引用。
          const handleClick = useCallback(() => {
            // 处理点击
          }, [dep1, dep2]); // 依赖项
          
          return <Child onClick={handleClick} />;
          
        • useMemo 缓存计算结果(包括对象)。依赖项不变时,返回同一个对象引用。
          const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
          
          return <Child data={memoizedValue} />;
          
    4. 状态提升与拆分:
      • 将频繁变化的状态下移到更具体的组件,避免导致不相关的上层组件重渲染。
      • 将大型组件拆分成更小的组件,让变化的影响范围局部化。
    5. Context 优化:
      • 避免在 Providervalue 中传递频繁变化的对象/函数,会导致所有消费组件重渲染。
      • 将 Context 拆分成多个,按需消费。
      • 消费组件用 React.memo 包裹(但要注意 value 变化仍会触发)。
    6. 列表渲染 key 给列表项稳定且唯一key,帮助 React 高效 Diff,避免不必要的重渲染。

🎯 总结口诀

  • HOC: 组件加工厂,穿外套加功能,现在 Hooks 更香。
  • 旧生命周期: componentWillReceiveProps 已凉,getDerivedStateFromProps + componentDidUpdate 上。
  • 渲染触发: setStateprops 变、父渲染、forceUpdate(慎用)。
  • 渲染过程: render 画新图,Diff 找差异,Commit 改 DOM。
  • 组件声明: 函数组件(主流)、类组件(传统)、createClass(古董)。
  • 有/无状态: 有状态管数据(记忆),无状态纯展示(傻显)。
  • Fragment: 空包裹 <></>,不生 DOM 节点,解决多元素和表格问题。
  • 取 DOM: useRef / createRef 绑元素,useEffect / componentDidMount 里访问。
  • render 访问 ref 不行!图纸阶段 DOM 还没生。
  • Portals: 传送门!渲染到别处,事件还认家(React 树)。Modal/Tooltip 救星。
  • 防重渲染: React.memoPureComponentshouldComponentUpdateuseCallback/useMemo 优化 props、拆组件、稳定 key

把这些核心点吃透,React 的性能优化和组件设计就能玩得很溜了!💪

  • 使用简洁的大白话讲解对 React-Intl 的理解,它的工作原理?对 React context 的理解,为什么React并不推荐优先考虑使用Context?React中什么是受控组件和非控组件?React中refs的作用是什么?有哪些应用场景?React组件的构造函数有什么作用?它是必须的吗?React.forwardRef是什么?它有什么作用?类组件与函数组件有什么异同?

好的!咱们用最简洁的大白话,把这些 React 核心概念串起来讲清楚,像吃串串一样,一串一串搞定!


🌍 1. 对 React-Intl 的理解?工作原理?

  • 大白话: React-Intl 是 React 的**“翻译官”。它帮你轻松实现应用的国际化(i18n),让你的 App 能根据用户设置自动切换语言**(比如中文、英文、日文)。
  • 工作原理:
    1. 准备“翻译字典”: 你把所有需要翻译的文字(按钮文字、提示语等)按不同语言写成 JSON 文件(如 zh-CN.json, en-US.json)。
      // zh-CN.json
      { "welcome": "欢迎", "button": "点击" }
      // en-US.json
      { "welcome": "Welcome", "button": "Click" }
      
    2. 配置“翻译官”: 在 App 顶层用 <IntlProvider> 包裹,告诉它当前用什么语言(locale)和对应的翻译字典(messages)。
      import { IntlProvider } from 'react-intl';
      import zhMessages from './zh-CN.json';
      
      function App() {
        return (
          <IntlProvider locale="zh-CN" messages={zhMessages}>
            <MyComponent />
          </IntlProvider>
        );
      }
      
    3. 组件“点菜”: 在组件里,不用写死文字,而是用 React-Intl 提供的组件或 Hook 去“翻译字典”里取文字。
      • 用组件: <FormattedMessage id="welcome" /> -> 显示“欢迎”
      • 用 Hook: const formatMessage = useIntl(); formatMessage({ id: 'button' }) -> 得到“点击”
    4. 自动切换: 改变 <IntlProvider>localemessages,整个 App 的文字就自动换成新语言了!

📦 2. 对 React Context 的理解?为什么不推荐优先使用?

  • 大白话: Context 是 React 提供的**“全家福”**。它允许你把一些数据(比如用户信息、主题色)直接“广播”给所有后代组件,不用一层一层用 props 传(避免“props drilling”)。
  • 怎么用?
    1. 创建“全家福相册”: const UserContext = createContext();
    2. 太爷爷发照片(Provider): 在顶层用 <UserContext.Provider value={userData}> 包裹后代。
    3. 重孙子看照片(Consumer): 在深层组件用 const user = useContext(UserContext); 直接拿到数据。
  • 为什么不推荐优先使用?
    • 性能陷阱(主要问题!): Context 的 Providervalue 一变,所有消费这个 Context 的组件都会强制重渲染!即使它们只用了 value 的一小部分。如果 value 是个复杂对象且频繁变化(比如 { user, settings, theme }),会导致大量无关组件重渲染,性能爆炸
    • 过度设计: 对于简单的父子/兄弟通信,用 props 或状态提升更直接、更清晰。Context 是“核武器”,小问题用大炮容易伤及无辜。
    • 组件耦合: 使用 Context 的组件和 Context 本身强耦合,降低了组件的复用性和可测试性(脱离 Context 环境就跑不了)。
    • 调试困难: 数据来源不直观,不如 props 追踪方便。
  • 什么时候用? 全局性、不常变的数据(如用户登录状态、主题、语言)。避免用于频繁变化的局部状态。

🎛️ 3. 受控组件 vs 非受控组件?

  • 大白话: 区别在于表单数据由谁管
  • 受控组件 (Controlled Component):
    • 特点: 表单数据(如输入框内容)完全由 React 的 state 控制
    • 流程:
      1. 给表单元素(如 <input>)设置 value={this.state.value}
      2. 监听 onChange 事件。
      3. 在事件处理函数里,用 setState 更新 state
      4. state 更新 -> 组件重渲染 -> 输入框显示新 value
    • 比喻: 像“遥控器”。输入框的值是“电视画面”,state 是“遥控器”。你按遥控器(setState)才能换台(改变值)。
    • 优点: 数据实时同步到 state,方便校验、处理、联动。
    • 缺点: 每次输入都触发 setState 和重渲染,可能影响性能(大量输入框时)。
  • 非受控组件 (Uncontrolled Component):
    • 特点: 表单数据由浏览器 DOM 自己管理。React 只在需要时(如提交时)通过 ref读取 DOM 的值。
    • 流程:
      1. 给表单元素设置 defaultValue(初始值)。
      2. ref 引用 DOM 元素。
      3. 需要值时(如提交按钮点击),通过 this.inputRef.current.value 读取。
    • 比喻: 像“自由人”。输入框自己管自己的值,React 不管。提交时去“问”它现在值是多少。
    • 优点: 代码简单,输入不触发重渲染(性能好),适合简单表单或文件上传。
    • 缺点: 实时校验、联动困难,数据不与 state 同步。
  • 怎么选? 优先用受控组件(数据流清晰,可控性强)。非受控组件用于简单场景(如搜索框、文件上传)或性能敏感的大量输入。

📍 4. React 中 refs 的作用?应用场景?

  • 大白话: ref 是 React 给你的“遥控器”,让你能直接操作真实的 DOM 元素访问类组件实例
  • 作用:
    1. 操作 DOM: 聚焦输入框、选中文字、播放媒体、测量尺寸位置等。
    2. 访问类组件实例: 调用类组件的方法(如 childRef.current.handleReset())。
    3. 存储可变值: 类似一个“不触发渲染的 state”(用 useRef 存定时器 ID、上一次的值等)。
  • 应用场景:
    • 聚焦输入框: inputRef.current.focus()
    • 触发动画: 通过 ref 获取 DOM 元素,调用动画库 API。
    • 集成第三方库: 很多非 React 库(如 D3.js, jQuery 插件)需要直接操作 DOM。
    • 访问类组件方法: 父组件通过 ref 调用子组件(类组件)的方法。
    • 存储值: const timerRef = useRef(); timerRef.current = setInterval(...);
  • ⚠️ 注意: 不要过度使用! 优先用 stateprops 驱动 UI。ref 是“逃生舱”,用于 React 声明式模型之外的必要操作。

⚙️ 5. React 组件的构造函数有什么作用?它是必须的吗?

  • 作用(类组件):
    1. 初始化 state this.state = { count: 0 }; (唯一正确地点!不要在 render 或其他地方直接改 this.state)。
    2. 绑定事件处理函数的 this this.handleClick = this.handleClick.bind(this); (避免 this 指向问题)。
    3. 初始化 ref this.myRef = React.createRef(); (虽然类属性语法更常用)。
    4. 调用 super(props) 必须第一行! 让子组件继承父组件的 props。不写 this.props 会是 undefined
  • 它是必须的吗?
    • 类组件: 不一定! 只有当你需要做上面提到的初始化工作(尤其是 statethis 绑定)时才需要写。
      • 如果组件没有 state,且不需要绑定 this(比如用箭头函数定义方法),可以省略构造函数
      • 如果需要初始化 state 或绑定 this必须写
    • 函数组件: 没有构造函数!useState Hook 初始化状态。

🔗 6. React.forwardRef 是什么?作用?

  • 大白话: forwardRef 是一个“穿针引线”的工具。它允许你把父组件传给子组件的 ref,再“转发”给子组件内部的某个元素或更深层的组件
  • 为什么需要? 默认情况下,ref 不能像 props 那样透传。父组件传 ref 给子组件 <Child ref={parentRef} />,子组件直接拿不到这个 ref(它被 React 保留用于指向子组件实例/DOM)。forwardRef 解决了这个问题。
  • 怎么用?
    // 子组件:用 forwardRef 包裹,ref 作为第二个参数接收
    const FancyButton = React.forwardRef((props, ref) => {
      // 把 ref 绑定到内部的 <button> 元素上
      return <button ref={ref} className="FancyButton">
        {props.children}
      </button>;
    });
    
    // 父组件:像平常一样使用 ref
    const ref = useRef();
    <FancyButton ref={ref}>Click me!</FancyButton>;
    
    // 现在 ref.current 指向 FancyButton 内部的 <button> DOM 元素!
    
  • 作用:
    1. 访问子组件内部 DOM: 父组件想直接操作子组件包裹的某个 DOM 元素(如上面的按钮)。
    2. 透传 ref 到更深层组件: 在高阶组件 (HOC) 或一些库组件中,需要把 ref 传递给被包裹的真实组件。
    3. 保持组件封装性: 子组件暴露一个 ref 给父组件,但内部具体结构可以自由变化,父组件只关心最终指向的元素。
  • 常见场景: 可复用的 UI 组件库(按钮、输入框)、动画库(需要操作内部 DOM)、高阶组件。

🔄 7. 类组件 vs 函数组件:异同?

特性类组件 (Class Component)函数组件 (Function Component)
定义方式class MyComp extends React.Componentfunction MyComp()const MyComp = () => {}
状态管理this.state / this.setState()Hooks: useState, useReducer
生命周期有明确生命周期方法 (componentDidMount 等)Hooks: useEffect (模拟生命周期)
this 指向this,需手动绑定事件处理函数this,没有绑定问题
实例每次渲染创建实例,实例在生命周期中存在无实例,每次渲染是一次函数调用
ref 访问this.ref 指向组件实例或 DOMuseRef 创建,.current 指向 DOM 或存储值
性能实例化开销略大,优化靠 shouldComponentUpdate轻量,优化靠 React.memo / useMemo / useCallback
逻辑复用Mixins (废弃) / HOC / Render Props自定义 Hooks (主流,更优雅)
代码风格面向对象 (OOP) 风格函数式编程 (FP) 风格
学习曲线需理解 this、生命周期需理解 Hooks 规则(闭包、依赖项)
当前地位传统方式,兼容旧代码主流 & 未来,React 官方推荐
  • 核心相同点:
    • 都是 React 组件,接收 props,返回 JSX (React Element)。
    • 都能管理状态和副作用(只是方式不同)。
    • 都能复用逻辑(HOC/Render Props vs Hooks)。
  • 核心不同点:
    • 心智模型: 类组件是“有状态的机器”(面向对象),函数组件是“无状态的函数 + Hooks”(函数式)。
    • this 类组件的“灵魂”也是“坑”,函数组件彻底摆脱 this
    • Hooks 革命: Hooks 让函数组件拥有了类组件的所有能力(状态、生命周期、上下文),并且逻辑复用更强大(自定义 Hooks)。这是函数组件成为主流的关键。
    • 简洁性: 函数组件通常更简洁,没有 this 绑定和生命周期方法的样板代码。

🎯 总结口诀

  • React-Intl: 翻译官,字典配,Provider 发,组件取。
  • Context: 全家福,免传参,但慎用!性能坑,耦合高。
  • 受控/非控: 受控靠 state (遥控器),非控靠 DOM (自由人)。优先受控。
  • refs 遥控器,操作 DOM/实例。存值不渲染。少用!
  • 构造函数: 类组件初始化 state/绑 this。非必须,按需写。
  • forwardRef 穿针线,转发 ref 到子组件内部。封装组件神器。
  • 类 vs 函数: 类有 this 和生命周期,函数靠 Hooks。函数是未来,Hooks 是王道!

把这些核心差异和使用场景记牢,React 的组件设计和状态管理就能玩得转了!💪

好的!咱们用最简洁的大白话,把 React 组件通信的“全家桶”按场景分类讲透,像剥洋葱一样一层层解决你的疑惑!


🏠 一、父子组件通信(最基础、最常用)

🧒 1. 父传子:爸爸给儿子塞零花钱

  • 方式: props(属性传递)
  • 大白话: 爸爸(父组件)直接把数据(零花钱)塞到儿子(子组件)口袋里(写在子组件标签的属性上)。儿子拿到钱(props)只能花(显示),不能自己印(改)。
  • 代码:
    // 爸爸 (父组件)
    function Dad() {
      const money = 100;
      return <Son money={money} />; // 塞钱!
    }
    
    // 儿子 (子组件)
    function Son(props) {
      return <p>我有 {props.money} 块!</p>; // 花钱!
    }
    

👦 2. 子传父:儿子找爸爸要钱

  • 方式: 回调函数(Callback Function)
  • 大白话: 儿子(子组件)不能直接改爸爸的钱包(state)。儿子想买玩具(需要父组件做事),就打电话(调用父组件传过来的回调函数)告诉爸爸:“爸,我想买玩具!”。爸爸接到电话(执行回调函数),然后自己去取钱(更新自己的 state),再把新零花钱(新数据)重新塞给儿子(重新渲染)。
  • 代码:
    // 爸爸 (父组件)
    function Dad() {
      const [totalMoney, setTotalMoney] = 1000);
    
      // 1. 爸爸定义“给钱规则”(回调函数)
      const handleGiveMoney = (amount) => {
        setTotalMoney(totalMoney - amount); // 爸爸掏钱
      };
    
      // 2. 把“规则”传给儿子
      return <Son onAskForMoney={handleGiveMoney} />;
    }
    
    // 儿子 (子组件)
    function Son(props) {
      const toyPrice = 50;
    
      return (
        <button onClick={() => props.onAskForMoney(toyPrice)}>
          爸,我要买玩具!
        </button>
      );
    }
    

🏢 二、跨级组件通信(爷爷 -> 重孙子)

📬 方式 1:Context(轻量级“全家福”)

  • 适用场景: 数据需要跨越多层组件传递(如主题色、用户信息),中间层组件用不到这些数据。
  • 大白话: 太爷爷(顶层组件)想给所有重孙子(深层组件)发红包(共享数据)。他不用一个个爸爸、爷爷地往下传,而是直接在家族群里(创建 Context) 发个公告:“所有重孙子都有红包!”。重孙子们只要加入这个群(使用 Context),就能直接看到公告(拿到数据),不用经过中间的爸爸、爷爷。
  • 代码:
    import { createContext, useContext } from 'react';
    
    // 1. 创建家族群 (Context)
    const FamilyContext = createContext();
    
    // 太爷爷 (顶层组件)
    function GreatGrandpa() {
      const familyName = "张家";
    
      return (
        // 2. 太爷爷在群里发公告 (Provider)
        <FamilyContext.Provider value={familyName}>
          <Grandpa />
        </FamilyContext.Provider>
      );
    }
    
    // 中间层组件 (爷爷/爸爸) - 完全不用知道 Context!
    function Grandpa() { return <Father />; }
    function Father() { return <Grandson />; }
    
    // 重孙子 (深层组件)
    function Grandson() {
      // 3. 重孙子加入群聊,直接看公告 (useContext)
      const name = useContext(FamilyContext);
      return <p>我们家姓 {name}</p>;
    }
    
  • ⚠️ 注意: Context 的 value 一变,所有消费它的组件都会强制重渲染!适合不常变的全局数据。

🏭 方式 2:状态提升 + Props 逐层传递(传统方式)

  • 适用场景: 数据需要共享,但层级不算特别深,或者 Context 不适用时。
  • 大白话: 把共享数据(state)和修改它的函数提升到最近的共同祖先组件。然后这个祖先组件像“中转站”,一层一层通过 props 把数据和方法传给需要它们的后代组件。
  • 代码: 类似“兄弟组件通信”中的状态提升,只是层级更深。中间层组件需要“透传”它们用不到的 props(有点麻烦)。

🌐 三、非嵌套关系组件通信(远房亲戚)

🏗️ 方式 1:状态提升到共同祖先(最推荐)

  • 适用场景: 两个组件在组件树中有共同的祖先(即使很远)。
  • 大白话: 就像兄弟组件通信的“放大版”。找到这两个“远房亲戚”最近的共同祖先,把共享状态和修改逻辑提升到这个祖先组件里。祖先组件再通过 props 把数据和方法分别传给这两个组件。
  • 优点: 符合 React 单向数据流原则,清晰可控。

📡 方式 2:全局状态管理(Redux / Zustand / MobX)

  • 适用场景: 应用复杂,多个不相关的组件需要共享和频繁修改大量状态(如购物车、用户信息)。状态提升到共同祖先变得困难或低效。
  • 大白话: 请个专业的“管家”(状态管理库)。管家住在独立的大房子里(Store),管着全家所有的钱、账本(全局状态)。任何组件(家庭成员)想:
    • 看账本(读状态): 直接问管家(通过 Hooks 如 useSelector)。
    • 买东西改账本(改状态): 填申请单(dispatch Action)交给管家。管家按规矩(Reducer)审批后修改账本。账本一改,管家通知所有关心这块账本的组件更新。
  • 代表库:
    • Redux: 最成熟,概念多(Store, Action, Reducer, Middleware),学习曲线陡峭。
    • Zustand: 轻量级,API 简洁,基于 Hooks,中小项目首选。
    • MobX: 响应式,更接近面向对象思维,自动追踪依赖。
  • 优点: 集中管理状态,解决复杂共享问题,调试工具强大(Redux DevTools)。
  • 缺点: 增加复杂度和包体积,小项目可能杀鸡用牛刀。

⚡ 方式 3:事件总线(Event Bus / 发布订阅 - 了解即可)

  • 适用场景: 极少数特殊场景,如两个完全独立的组件需要临时通信,且不想引入全局状态管理。React 中不推荐!
  • 大白话: 像村里的广播站。组件 A 往广播站发消息(emit),组件 B 订阅(on)这个消息。消息发出后,组件 B 就能收到并执行操作。
  • 简单实现:
    // 创建一个简单的“事件总线”
    const eventBus = {
      listeners: {},
      on(event, callback) {
        if (!this.listeners[event]) this.listeners[event] = [];
        this.listeners[event].push(callback);
      },
      emit(event, data) {
        if (this.listeners[event]) {
          this.listeners[event].forEach(callback => callback(data));
        }
      }
    };
    
    // 组件 A:发消息
    function ComponentA() {
      const handleClick = () => eventBus.emit('messageFromA', '你好!');
      return <button onClick={handleClick}>发送消息</button>;
    }
    
    // 组件 B:收消息
    function ComponentB() {
      const [message, setMessage] = useState('');
      useEffect(() => {
        eventBus.on('messageFromA', (data) => setMessage(data));
      }, []);
      return <p>收到消息: {message}</p>;
    }
    
  • ⚠️ 为什么不推荐?
    • 破坏 React 单向数据流,数据流向不清晰,难以调试。
    • 容易导致内存泄漏(忘记取消订阅)。
    • 组件间隐式耦合,降低可维护性。
    • 在 React 生态中,Context 或状态管理库是更符合其设计哲学的方案。

🕳️ 四、如何解决 Props 层级过深的问题?(Props Drilling)

  • 大白话: “Props Drilling” 就是为了把数据传给深层组件,中间层被迫接收并传递它们自己根本用不到的 props。就像送快递,为了把包裹送到 10 楼,2-9 楼都得签收再转交,很麻烦。

✅ 解决方案(按推荐顺序)

  1. 🥇 首选:React Context

    • 适用场景: 数据是全局性、不常变的(主题、用户信息、语言)。
    • 优点: React 内置,轻量级,API 简单(尤其 useContext Hook)。
    • 代码: 见上面“跨级通信 - Context”部分。中间层组件完全不用管这些 props
  2. 🥈 次选:状态管理库(Redux / Zustand)

    • 适用场景: 应用复杂、状态共享频繁、数据变化多(购物车、复杂表单、实时数据)。
    • 优点: 强大、集中管理、调试工具好、社区成熟。
    • 代码(以 Zustand 为例):
      import { create } from 'zustand';
      
      // 1. 创建 Store (大房子)
      const useStore = create((set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
      }));
      
      // 组件 A (任何地方):读状态
      function ComponentA() {
        const count = useStore((state) => state.count);
        return <p>Count: {count}</p>;
      }
      
      // 组件 B (任何地方):改状态
      function ComponentB() {
        const increment = useStore((state) => state.increment);
        return <button onClick={increment}>+1</button>;
      }
      
    • 优点: 彻底解决 Props Drilling,组件直接访问 Store,无需层层传递。
  3. 🥉 再次:组件拆分与状态提升

    • 适用场景: 仔细分析状态的真实归属。有时状态提升的位置可以更合理,或者组件可以拆分得更小,减少传递层级。
    • 优点: 符合 React 设计哲学,不引入额外依赖。
    • 缺点: 对于深层嵌套且确实需要共享的状态,效果有限。
  4. 🤔 考虑:组合模式(Composition)

    • 适用场景: 利用 JSX 的灵活性,将子组件作为 props.children 或特定 prop 传递。
    • 大白话: 父组件不直接传数据,而是把“渲染权”交给子组件。父组件提供“插槽”,子组件自己决定怎么渲染。
    • 代码(类似 Render Props 思想):
      // 父组件:提供“数据”和“渲染位置”
      function DataProvider({ children }) {
        const data = { user: "张三" };
        // children 是个函数,接收 data 并返回 JSX
        return <div>{children(data)}</div>;
      }
      
      // 使用
      <DataProvider>
        {(data) => <p>用户: {data.user}</p>} {/* 子组件决定怎么渲染 */}
      </DataProvider>
      
    • 优点: 避免显式传递 props,更灵活。
    • 缺点: 写法稍显复杂,可能增加嵌套。

📊 五、组件通信方式总结(决策树)

graph TD
    A[需要通信的组件关系?] -->|父子| B[父传子: props<br>子传父: 回调函数]
    A -->|跨级/多层| C{数据全局/不常变?}
    C -->|是| D[React Context]
    C -->|否/复杂| E[状态管理库 Redux/Zustand]
    A -->|非嵌套/无共同祖先| F{应用复杂/状态多?}
    F -->|是| E
    F -->|否/简单| G[状态提升到共同祖先<br>或 事件总线(不推荐)]
    B --> H{Props 层级过深?}
    H -->|是| C
    H -->|否| I[保持 props 传递]

🎯 核心口诀

  1. 父子通信: props 传数据,回调函数传操作(单向数据流!)。
  2. 跨级通信: Context 是轻量首选(全家福),状态管理库 是复杂利器(大管家)。
  3. 非嵌套通信: 状态提升找共同祖先,状态管理库 解决复杂全局状态,事件总线(了解,少用)。
  4. 解决 Props Drilling: Context状态管理库 是核武器!组件拆分状态提升是基本功。

掌握这些通信方式,你就能在 React 的组件世界里游刃有余地“传话办事”啦!💪