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

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

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


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

  • 大白话: 组件就是一个独立、可复用的小零件!就像乐高积木里的一块砖、一个轮子、一个窗户。你可以用这些小零件拼出大汽车、大房子(整个应用)。
  • 为什么重要? React 的核心思想就是**"万物皆组件"**!把复杂的页面拆成一个个功能明确的小组件,这样:
    • 好维护: 哪块砖坏了,换掉那块就行,不用拆整栋楼。
    • 好复用: 一个轮子组件,可以在汽车上用,可以在飞机上用,不用每次都重新造轮子。
    • 好理解: 看到一堆小零件组合,比看一坨乱麻的代码清晰多了。
  • 两种写法(现在主流是第一种):
    1. 函数组件 (最常用!像普通员工):

      jsx 复制代码
      function Welcome(props) {
        return <h1>你好, {props.name}!</h1>; // 返回要显示的内容
      }
      • 大白话: 就是一个普通的 JavaScript 函数!它接收一个叫 props 的"快递包裹"(后面讲),然后返回 一段用 JSX(后面讲)写的"界面代码"。简单、直接、现在最流行!
    2. 类组件 (像部门经理,带状态和生命周期):

      jsx 复制代码
      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 变量、计算、函数调用,必须用花括号 {} 包起来!

      jsx 复制代码
      const name = "小明";
      const element = <h1>你好, {name}!</h1>; // 正确!
      // const element = <h1>你好, name!</h1>; // 错误!会显示字符串 "name"
    2. className 代替 class 因为 class 是 JavaScript 的关键字(用来定义类),所以 JSX 里写样式类名要用 className

      jsx 复制代码
      // 错误!
      // <div class="container">内容</div>
      // 正确!
      <div className="container">内容</div>
    3. 标签必须闭合:<img>, <br> 这种 HTML 里可以不闭合的标签,在 JSX 里必须 写成自闭合形式 <img />, <br />

    4. 根元素只能有一个: 一个组件的 JSX 返回,最外层必须 有且只有一个父标签。如果不想多一个无意义的 div,可以用 <></> (Fragment) 包起来。

      jsx 复制代码
      // 错误!
      // 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. 父组件传值: 在子组件标签上写属性名=值,就像给快递贴单子。

      jsx 复制代码
      function App() {
        return <Welcome name="小红" age={18} />; // 传字符串用引号,传数字用{}
      }
    2. 子组件接收: 函数组件通过参数接收(通常叫 props),类组件通过 this.props 接收。

      jsx 复制代码
      // 函数组件接收
      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 (最常用!)

      jsx 复制代码
      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()

      jsx 复制代码
      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. 绑定函数: 事件处理函数通常用 {} 包裹一个函数。

      jsx 复制代码
      // 函数组件
      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:构造函数里绑定 (推荐)

        jsx 复制代码
        class Button extends React.Component {
          constructor(props) {
            super(props);
            this.handleClick = this.handleClick.bind(this); // 绑定!
          }
        
          handleClick() {
            console.log(this); // 现在是 Button 组件实例了
          }
          // ... render
        }
      • 方法2:使用箭头函数 (类属性语法)

        jsx 复制代码
        class Button extends React.Component {
          handleClick = () => { // 箭头函数自动绑定 this
            console.log(this); // 是 Button 组件实例
          }
          // ... render
        }
      • 方法3:在 JSX 里用箭头函数 (不推荐,每次渲染都创建新函数,可能影响性能)

        jsx 复制代码
        render() {
          return <button onClick={() => this.handleClick()}>点我</button>;
        }
    4. 传递参数: 想在事件处理函数里传额外参数?

      • 函数组件: 用箭头函数包裹。

        jsx 复制代码
        function DeleteButton({ id }) {
          const handleDelete = (idToDelete) => {
            console.log(`要删除 ID 为 ${idToDelete} 的项目`);
          };
        
          return (
            <button onClick={() => handleDelete(id)}>
              删除
            </button>
          );
        }
      • 类组件: 同样用箭头函数包裹,或者用 .bind(this, 参数)

        jsx 复制代码
        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 之前判断。

      jsx 复制代码
      function Greeting({ isLoggedIn }) {
        if (isLoggedIn) {
          return <h1>欢迎回来!</h1>;
        } else {
          return <h1>请先登录。</h1>;
        }
      }
    2. 三元运算符 ? : 简洁,适合在 JSX 里直接写。

      jsx 复制代码
      function Greeting({ isLoggedIn }) {
        return (
          <div>
            {isLoggedIn ? (
              <h1>欢迎回来!</h1>
            ) : (
              <h1>请先登录。</h1>
            )}
          </div>
        );
      }
    3. 逻辑与 && 当条件为 true 时才渲染后面的内容(常用于显示/隐藏)。

      jsx 复制代码
      function Notification({ unreadCount }) {
        return (
          <div>
            <h1>通知中心</h1>
            {unreadCount > 0 && (
              <span className="badge">{unreadCount}</span>
            )}
          </div>
        );
      }
      // 如果 unreadCount 是 0,<span> 就不会渲染

📋 七、列表渲染 ------ 批量生产"零件"

  • 大白话: 当你有一组数据(比如数组),需要根据这组数据生成一组相同的组件结构(比如列表项)时用的。就像工厂流水线,根据图纸(数据)生产出一堆一模一样的零件(列表项)。

  • 核心方法:用数组的 .map() 方法

    jsx 复制代码
    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) 是啥?

    • 大白话: 只比较第一层 。对于基本类型(数字、字符串、布尔值),直接比值是否相等。对于对象/数组,只比引用地址 是否相同(是不是同一个对象),不会深入比较对象内部属性是否变了

      js 复制代码
      // 浅比较结果
      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 Component this.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 节点:

      jsx 复制代码
      // 错误!需要根元素
      // 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 元素)
  • 例子:

    jsx 复制代码
    // 函数组件
    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 的正确时机!
  • 错误示例:

    jsx 复制代码
    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

      jsx 复制代码
      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 缓存函数 。依赖项不变时,返回同一个函数引用。

          jsx 复制代码
          const handleClick = useCallback(() => {
            // 处理点击
          }, [dep1, dep2]); // 依赖项
          
          return <Child onClick={handleClick} />;
        • useMemo 缓存计算结果 (包括对象)。依赖项不变时,返回同一个对象引用。

          jsx 复制代码
          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)。

      json 复制代码
      // zh-CN.json
      { "welcome": "欢迎", "button": "点击" }
      // en-US.json
      { "welcome": "Welcome", "button": "Click" }
    2. 配置"翻译官": 在 App 顶层用 <IntlProvider> 包裹,告诉它当前用什么语言(locale)和对应的翻译字典(messages)。

      jsx 复制代码
      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 解决了这个问题。

  • 怎么用?

    jsx 复制代码
    // 子组件:用 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.Component function MyComp()const MyComp = () => {}
状态管理 this.state / this.setState() Hooks : useState, useReducer
生命周期 有明确生命周期方法 (componentDidMount 等) Hooks : useEffect (模拟生命周期)
this 指向 this,需手动绑定事件处理函数 this,没有绑定问题
实例 每次渲染创建实例,实例在生命周期中存在 无实例 ,每次渲染是一次函数调用
ref 访问 this.ref 指向组件实例或 DOM useRef 创建,.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)只能花(显示),不能自己印(改)。

  • 代码:

    jsx 复制代码
    // 爸爸 (父组件)
    function Dad() {
      const money = 100;
      return <Son money={money} />; // 塞钱!
    }
    
    // 儿子 (子组件)
    function Son(props) {
      return <p>我有 {props.money} 块!</p>; // 花钱!
    }

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

  • 方式: 回调函数(Callback Function)

  • 大白话: 儿子(子组件)不能直接改爸爸的钱包(state)。儿子想买玩具(需要父组件做事),就打电话 (调用父组件传过来的回调函数 )告诉爸爸:"爸,我想买玩具!"。爸爸接到电话(执行回调函数),然后自己 去取钱(更新自己的 state),再把新零花钱(新数据)重新塞给儿子(重新渲染)。

  • 代码:

    jsx 复制代码
    // 爸爸 (父组件)
    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),就能直接看到公告(拿到数据),不用经过中间的爸爸、爷爷。

  • 代码:

    jsx 复制代码
    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 就能收到并执行操作。

  • 简单实现:

    jsx 复制代码
    // 创建一个简单的"事件总线"
    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 为例):

      jsx 复制代码
      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 思想):

      jsx 复制代码
      // 父组件:提供"数据"和"渲染位置"
      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
子传父: 回调函数] A -->|跨级/多层| C{数据全局/不常变?} C -->|是| D[React Context] C -->|否/复杂| E[状态管理库 Redux/Zustand] A -->|非嵌套/无共同祖先| F{应用复杂/状态多?} F -->|是| E F -->|否/简单| G[状态提升到共同祖先
或 事件总线(不推荐)] B --> H{Props 层级过深?} H -->|是| C H -->|否| I[保持 props 传递]

🎯 核心口诀

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

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

相关推荐
樱花开了几轉5 小时前
React中为甚么强调props的不可变性
前端·javascript·react.js
风清云淡_A6 小时前
【REACT18.x】CRA+TS+ANTD5.X实现useImperativeHandle让父组件修改子组件的数据
前端·react.js
小飞大王6666 小时前
React与Rudex的合奏
前端·react.js·前端框架
若梦plus6 小时前
React之react-dom中的dom-server与dom-client
前端·react.js
若梦plus6 小时前
react-router-dom中的几种路由详解
前端·react.js
讨厌吃蛋黄酥11 小时前
React样式冲突终结者:CSS模块化+Vite全链路实战指南🔥
前端·javascript·react.js
tianchang12 小时前
React Hook 解析(一):useCallback 与 useMemo
前端·react.js
一颗奇趣蛋13 小时前
React- useMemo & useCallback
前端·react.js
樱花开了几轉14 小时前
React中的合成事件解释和理解
前端·javascript·react.js