Skip to content

1. React 事件机制

React 事件机制主要基于一个称为 SyntheticEvent(合成事件)的系统来统一不同浏览器上的事件表现,同时使用事件委托(event delegation)机制来提升性能。

以下将从几个方面为你详细介绍 React 事件机制的核心原理与常见用法。


1. React 中事件机制的特点

  1. 合成事件(SyntheticEvent)

    • React 并不直接将原生 DOM 事件对象暴露给你,而是使用一个名为 SyntheticEvent 的事件包装器。
    • 该合成事件可以保证在不同浏览器环境下有一致的事件属性和方法,比如 target, type, pageX, pageY 等都被统一封装。
    • 早期版本的 React(17 以下)使用了 事件池(Event Pooling) 技术,每次触发事件后会将合成事件对象内容重置以复用对象,避免频繁创建和销毁对象。但从 React 17 开始已经逐渐移除了事件池,你可以随时异步访问事件对象的属性
  2. 事件委托(Event Delegation)

    • React 并不是在每个真实 DOM 节点上直接绑定事件监听器,而是采用事件委托的方式:
      • 旧的实现(React < 17)通常将所有事件统一绑定到 documentwindow 上;
      • React 17+ 将事件监听器绑定到React 根节点(根容器)上。
    • 通过事件冒泡(事件从子元素冒泡到父元素)来捕获事件,再由 React 的内部机制去分发给对应的组件。
    • 这种机制可以减少大量事件监听器的注册,提升性能。
  3. 跨浏览器一致性

    • 由于使用了 SyntheticEvent,React 的事件在各种浏览器中都有相同的属性和行为,你无需针对不同浏览器做不同的兼容处理。
  4. 在 JSX 中使用 CamelCase 命名

    • React 事件的命名方式与原生 DOM 事件的不同之处在于需要使用“驼峰命名”,如 onClickonChangeonMouseOver 等,而原生 DOM 中往往是 onclickonchangeonmouseover 等小写形式

2. React 事件绑定示例

在 JSX 中,你可以直接在组件的元素上使用事件属性来绑定事件处理函数。例如:

jsx
function App() {
  function handleClick(event) {
    // event 是一个合成事件对象 (SyntheticEvent)
    console.log('按钮被点击了');
    // 阻止默认行为
    event.preventDefault();
    // 阻止事件冒泡
    event.stopPropagation();
  }

  return (
    &lt;button onClick=&#123;handleClick&#125;&gt;
      点击我
    &lt;/button&gt;
  );
}

事件处理与状态更新

React 里通常会将事件和 setState/useState 状态更新结合使用,来实现页面交互。例如:

jsx
import React, &#123; useState &#125; from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(count + 1);
  }

  return (
    &lt;div&gt;
      &lt;p&gt;当前计数:&#123;count&#125;&lt;/p&gt;
      &lt;button onClick=&#123;handleIncrement&#125;&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default Counter;

3. 事件传播与冒泡/捕获

3.1 冒泡阶段

在默认情况下,当一个子元素触发事件后,事件会沿着 DOM 树逐级向上直到根节点或被手动阻止为止,这个过程称为事件冒泡

在 React 中,你也可以使用合成事件对象的 event.stopPropagation() 来阻止事件向父级冒泡。

3.2 捕获阶段

React 也支持事件捕获阶段,但需要在事件名后加上 Capture 关键字。例如:

jsx
&lt;div onClickCapture=&#123;handleCapture&#125;&gt;...&lt;/div&gt;

在捕获阶段注册的回调会比冒泡阶段更先触发。


4. React 17+ 与事件绑定变化

React 17+ 中,React 改变了事件的委托方式:

  • React 17 之前:默认把所有事件监听器绑定到 document 上。
  • React 17 及之后:把事件监听器绑定到根容器(也就是应用挂载的 root DOM 节点)上。

对大部分使用者而言,这个变化在日常开发里影响不大,但它让多版本 React多个 React 应用共存时更稳定,也更容易与非 React 应用协作。

如果你在外部手写 document.addEventListener,尤其是在同一条事件传播链里再做 stopPropagation(),就要注意它可能影响 React 内部的事件流。


5. 常见事件类型

React 对常见的事件类型都提供了支持,常见的一些分类如下:

  1. 鼠标事件
    • onClick, onDoubleClick, onMouseDown, onMouseMove, onMouseUp, onMouseEnter, onMouseLeave, onContextMenu 等。
  2. 键盘事件
    • onKeyDown, onKeyPress, onKeyUp 等。
  3. 表单事件
    • onChange, onInput, onSubmit, onFocus, onBlur 等。
  4. 焦点事件
    • onFocus, onBlur 等。
  5. 拖拽事件
    • onDrag, onDragStart, onDragOver, onDrop 等。
  6. 触摸事件(移动端)
    • onTouchStart, onTouchMove, onTouchEnd 等。

对于每种事件,React 都提供了一个合成事件对象,拥有通用的事件属性和方法。


6. 防止默认行为和事件冒泡

在原生 DOM 中,你可以使用 event.preventDefault() 来阻止默认行为,比如点击链接时阻止跳转;用 event.stopPropagation() 来阻止事件冒泡。在 React 的合成事件中也可以这么使用:

jsx
function LinkButton() {
  function handleClick(e) {
    e.preventDefault(); // 阻止默认跳转
    console.log('阻止了默认链接跳转行为');
  }

  return (
    &lt;a href="https://example.com" onClick=&#123;handleClick&#125;&gt;
      点击尝试跳转
    &lt;/a&gt;
  );
}

7. 事件处理函数的绑定方式

  1. 内联声明
jsx
&lt;button onClick=&#123;() =&gt; console.log('Clicked!')&#125;&gt;点击&lt;/button&gt;
  • 直接使用箭头函数,简洁方便,但如果过多使用会在每次渲染时创建新的函数对象,可能在一些场景下引发性能或子组件重复渲染问题(可配合 memo 或者父组件中的函数绑定来优化)。
  1. 绑定到组件实例方法
jsx
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // 构造函数中绑定
  }

  handleClick() {
    console.log('class-based click');
  }

  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;点击&lt;/button&gt;;
  }
}
  • 传统 class 组件写法,需要在构造函数中手动 .bind(this)
  1. 使用类的公共字段语法(class fields)
jsx
class MyComponent extends React.Component {
  handleClick = () =&gt; {
    console.log('public class field');
  };

  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;点击&lt;/button&gt;;
  }
}
  • 这种写法可以避免在构造函数中绑定 this,语法更简洁,但需要 Babel 或较新的环境支持。
  1. 函数式组件(Hook)
jsx
function MyButton() {
  const handleClick = () =&gt; {
    console.log('function component click');
  };

  return &lt;button onClick=&#123;handleClick&#125;&gt;点击&lt;/button&gt;;
}
  • 在函数式组件中,事件处理函数就是一个闭包,搭配 Hooks 可使用组件内的状态、上下文等信息。

8. 事件与状态的异步更新

React 的状态更新(setState/useState)是异步批量处理的,这意味着在事件回调里多次更新状态,可能不会立刻生效,需要等到本次事件回合结束后再统一执行更新并重新渲染。因此,你往往需要使用回调形式的更新函数来确保使用最新的 state:

jsx
function Counter() {
  const [count, setCount] = useState(0);

  function handleMultipleUpdates() {
    // 连续多次使用旧状态会造成问题,所以要使用回调函数
    setCount((prevCount) =&gt; prevCount + 1);
    setCount((prevCount) =&gt; prevCount + 1);
  }

  return (
    &lt;div&gt;
      &lt;p&gt;当前计数:&#123;count&#125;&lt;/p&gt;
      &lt;button onClick=&#123;handleMultipleUpdates&#125;&gt;加2&lt;/button&gt;
    &lt;/div&gt;
  );
}

9. 小结

  1. 合成事件:React 使用 SyntheticEvent 封装了浏览器原生事件,实现了跨浏览器一致性,并提供了类似 DOM 事件的属性和方法。
  2. 事件委托:React 利用事件冒泡在根容器统一处理事件监听,减少 DOM 上的事件绑定数量并提升性能。
  3. 事件绑定:在 JSX 中使用 onClickonChange 等属性结合函数式或 class 组件进行事件处理。
  4. 阻止默认与冒泡:依旧使用 event.preventDefault()event.stopPropagation()
  5. React 17+ 的变化:主要是将委托事件的绑定位置改为 React 应用的根容器,以便多应用、不同 React 版本共存。
  6. 异步状态更新:在事件处理函数内更新状态时,需注意 React 的状态是批量异步更新,需要使用正确的方式(如回调形式)来获取最新 state。

正确理解和使用 React 事件系统,可以帮助你在开发中更好地处理交互逻辑,写出性能高、可维护性好的前端代码。

2. React 的事件和普通的 HTML 事件有什么不同?

可以从 4 个点来回答:

  • 命名方式不同:原生 DOM 常见写法是 onclickonchange 这类全小写;React 在 JSX 中使用 onClickonChange 这样的 camelCase。
  • 绑定方式不同:HTML 内联事件常写成字符串;React 传入的是函数。
  • 阻止默认行为的方式不同:React 里不要依赖 return false,而是显式调用 event.preventDefault()
  • 事件对象不同:React 回调里拿到的是 SyntheticEvent,它对浏览器差异做了统一封装。

补充两点更稳妥:

  • 早期 React 的确把大部分事件委托到 document;React 17+ 改成委托到 React 根容器。
  • 所以“原生事件一定先执行、React 事件一定后执行,并且一定冒泡到 document”这种说法只适用于旧实现,不能作为今天的通用结论。

实战里最好避免在同一条传播链上混用 React 事件和手写 addEventListener,否则一旦出现 stopPropagation()、捕获/冒泡混用,排查会很麻烦。

3. React 组件中怎么做事件代理?它的原理是什么?

在 React 组件里,通常不需要手写事件代理。你只需要在 JSX 上声明 onClickonChange 之类的回调,React 会在底层帮你完成事件委托和分发。

可以把它理解成 3 步:

  • 统一事件对象:React 使用 SyntheticEvent 包装原生事件,屏蔽不同浏览器之间的细节差异。
  • 统一注册监听器:React 不会给每个 DOM 节点都绑一个监听器,而是按事件类型在根容器注册较少量的监听器。
  • 事件分发:事件发生后,React 会根据当前 Fiber 树和目标节点,把回调分发给对应组件。

补一句容易答错的点:

  • “自动绑定 this” 不是 React 事件系统的能力。 这是 React.createClass 时代的历史行为;
  • ES6 class 组件的方法默认并不会自动绑定 this

4. React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代

这三者是目前react解决代码复用的主要方式:

  • 高阶组件(HOC): React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
  • render props:一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
  • 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

(1)HOC 官方解释∶

> 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

简言之,HOC是一种组件的设计模式,HOC接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。

js
// hoc的定义
function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    // 一些通用的逻辑处理
    render() {
      // ... 并使用新数据渲染被包装的组件!
      return &lt;WrappedComponent data=&#123;this.state.data&#125; &#123;...this.props&#125; /&gt;;
    }
  };

// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
  (DataSource, props) =&gt; DataSource.getBlogPost(props.id));

HOC的优缺点∶

  • 优点∶ 逻辑复用、不影响被包裹组件的内部逻辑。
  • 缺点∶ hoc传递给被包裹组件的props容易和被包裹后的组件重名,进而被覆盖

(2)Render props 官方解释∶

> "render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。

js
// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components {
     state = {
    name: 'Tom'
  }

    render() {
    return (
        &lt;div&gt;
          &lt;p&gt;共享数据组件自己内部的渲染逻辑&lt;/p&gt;
          &#123; this.props.render(this.state) &#125;
      &lt;/div&gt;
    );
  }
}

// 调用方式
&lt;DataProvider render={data =&gt; (
  &lt;h1&gt;Hello &#123;data.name&#125;&lt;/h1&gt;
)}/&gt;

由此可以看到,render props的优缺点也很明显∶

  • 优点:数据共享、代码复用,将组件内的state作为props传递给调用者,将渲染逻辑交给调用者。
  • 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅

(3)Hooks 官方解释∶

> Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。

js
// 自定义一个获取订阅数据的hook
function useSubscription() {
  const data = DataSource.getComments();
  return [data];
}
// 
function CommentList(props) {
  const &#123;data&#125; = props;
  const [subData] = useSubscription();
    ...
}
// 使用
&lt;CommentList data='hello' /&gt;

以上可以看出,hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶

  • 使用直观;
  • 解决hoc的prop 重名问题;
  • 解决render props 因共享数据 而出现嵌套地狱的问题;
  • 能在return之外使用数据的问题。

需要注意的是:hook只能在组件顶层使用,不可在分支语句中使用。

总结∶ Hoc、render props和hook都是为了解决代码复用的问题,但是hoc和render props都有特定的使用场景和明显的缺点。hook是react16.8更新的新的API,让组件逻辑复用更简洁明了,同时也解决了hoc和render props的一些缺点。

5. 对React-Fiber的理解,它解决了什么问题?

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

6. React.Component 和 React.PureComponent 的区别

React.ComponentReact.PureComponent 的主要区别在于是否实现了浅层比较(shallow compare) 来决定是否需要更新组件。

下面是二者的具体对比:


1. React.Component

  • 基础类:React.Component 是 React 提供的最基本的类组件基类,用于创建传统的 class 组件。
  • 组件更新:在其默认实现中,每当父组件重新渲染,或 setState() 更新状态时,都不会自动进行任何性能优化,都会触发 render()
    • 如果想要控制组件的更新逻辑,可以手动在组件里实现 shouldComponentUpdate(nextProps, nextState) 方法,用以决定是否重新渲染。
  • 使用场景
    • 当你需要灵活控制更新流程、或有更加复杂的渲染逻辑时,可以使用 React.Component 并自行实现生命周期优化。

2. React.PureComponent

  • 自动浅比较:React.PureComponent 内部自动实现了 shouldComponentUpdate(),会对新旧的 props 和 state 进行浅层比较(shallow compare)。
    • 只有当 props 或 state 中的任意一个顶层值有变化时,才会触发重新渲染。
    • “浅比较”意味着只比较对象是否是同一个引用,而不比较对象内部更深层次的属性。
  • 性能优化
    • 因为自动跳过了相同引用对象场景下的重复渲染,所以在一些数据不可变(Immutable)场景中能够带来明显性能收益。
    • 使用不当也有风险:如果 props 或 state 是可变的深层嵌套对象,PureComponent 无法检测到内部的变化,就可能会产生“更新丢失”问题(即实际上对象内部值变了,但引用没变,也就不会触发重新渲染)。
  • 使用场景
    • 适合纯粹展示型组件或数据结构稳定、可预期的场景(如不可变数据、浅层对象或基本类型等)。
    • 在需要深层次比较或数据不可控的场景时,需要谨慎使用。

3. 使用示例对比

3.1 React.Component 示例

jsx
class NormalComponent extends React.Component {
  // 未实现 shouldComponentUpdate,默认每次父组件重新渲染都调用 render()
  render() {
    console.log("NormalComponent render");
    return &lt;div&gt;普通组件&lt;/div&gt;;
  }
}

3.2 React.PureComponent 示例

jsx
class PureDemo extends React.PureComponent {
  // PureComponent 内部已自动实现浅比较
  render() {
    console.log("PureDemo render");
    return &lt;div&gt;Pure 组件&lt;/div&gt;;
  }
}

比较逻辑

  • 当父组件更新时,React 会先对 this.propsnextProps(以及 this.statenextState)进行浅比较,只有在浅比较发现不相同时,PureDemo 才会执行 render()

4. 何时使用 PureComponent

  1. 数据不可变
    • 当组件的 state 或 props 都使用不可变数据结构(immutable data),每次更新都会返回新的对象引用,这时 PureComponent 的浅比较就能精准检测到差异,从而减少无谓渲染。
  2. 组件纯展示
    • 如果组件大部分情况下只依赖简单的 props 或基本类型,PureComponent 可以减少重复渲染开销。
  3. 性能瓶颈
    • 如果应用中存在组件渲染开销较大且会频繁渲染时,可考虑将其转换为 PureComponent,以提升性能。

5. 潜在的风险或注意事项

  1. 深层对象变化
    • 由于使用的是浅比较,如果传递给组件的 props 是一个深层嵌套的可变对象,即使对象内部内容被修改了,只要对象引用未变,都不会被检测到。
    • 结果可能导致 UI 不更新或产生逻辑错误。
  2. 无效渲染减少
    • 如果组件本身更新代价不大,使用 PureComponent 带来的收益可能并不明显,甚至会损耗一些因为浅比较而带来的性能开销(通常这一开销比较小,但在低端设备或者极频繁的场景中也需考虑)。
  3. 过早优化
    • 只有在遇到明显的性能瓶颈,或需求场景适合时,才应该考虑 PureComponent。切记过度优化会增加代码复杂度。

6. 小结

  • React.Component:没有自动的性能优化,需要手动编写 shouldComponentUpdate 或其他方法来控制渲染过程。
  • React.PureComponent:内置浅比较逻辑,适合不可变数据或较为简单的场景,可在无需额外编码的情况下减少不必要的渲染,从而提升性能。

选择使用 React.Component 还是 React.PureComponent,主要取决于数据是否不可变、是否有深层对象,以及是否确实需要这层优化。根据应用情况灵活选择即可。

7. Component, Element, Instance 之间有什么区别和联系?

在 React 中,ComponentElementInstance 是三个既相互关联又有明确分工的概念。它们主要对应 React 内部从定义到创建,再到渲染(以及重新渲染)的不同阶段或不同抽象。下面详细介绍它们的区别和联系。


1. Component(组件)

  • 定义/描述

    • React 组件可以是一个 函数组件(Function Component),也可以是一个 类组件(Class Component)
    • 组件定义了渲染逻辑以及 UI 的结构和交互,本质上是一个可复用的代码单元
  • 工作方式

  1. 函数组件:就是一个返回 React 元素(或其他组件)的函数:
jsx
function MyButton(props) {
  return &lt;button&gt;&#123;props.label&#125;&lt;/button&gt;;
}
  1. 类组件:本质是继承自 React.ComponentReact.PureComponent 的一个类,需要实现 render() 方法:
jsx
class MyButton extends React.Component {
  render() {
    return &lt;button&gt;&#123;this.props.label&#125;&lt;/button&gt;;
  }
}
  • 用途

    1. 组件就是构建 UI 的基本单元
    2. 允许你在应用中拆分、组合和复用 UI 逻辑。
  • 与 Element/Instance 的关系

    1. 在运行时,组件会被 React 调用(或者说“实例化”),从而返回一个或多个 React Element
    2. 对于类组件来说,React 也会创建组件的实例对象来管理生命周期和状态。
    3. 对于函数组件来说,并不会创建传统的“实例对象”,但内部依然有对应的数据结构(fiber)来跟踪其状态和渲染过程。

2. Element(元素)

  • React 元素 = 用于描述 UI 的轻量对象

    • React 元素是一个纯粹的、不可变的 JavaScript 对象,它包含以下关键信息:
      • type:可以是一个字符串(如 "div",表示 DOM 标签)或者一个 React 组件(如 MyButton)。
      • props:组件/标签的属性对象。
      • key:列表渲染时用于标识元素的唯一性。
      • 等等。
  • 创建方式

    • 通过 JSX 语法或 React.createElement 来创建 React 元素。
    • 例如,JSX &lt;div className="box" /&gt; 最终被编译为 React.createElement('div', &#123; className: 'box' &#125;, null)
    • 或者,一个函数组件 &lt;MyButton label="Click Me" /&gt; 则对应 React.createElement(MyButton, &#123; label: 'Click Me' &#125;, null)
  • 特点

    1. 不可变:一旦创建,元素对象的内容就不会变更。如果需要更新 UI,React 会创建新的元素并对比新旧元素(通过虚拟 DOM diff)。
    2. 描述层:可以把 React 元素理解为一份 “视图的声明”,即告诉 React “想要渲染什么”
  • 与 Component/Instance 的关系

    1. 元素在 React 中是最基本的组成单位,组件的 render() 方法就是返回 React 元素
    2. React 在执行过程中,会根据元素的 type 判断是原生 DOM 标签还是自定义组件。如果是自定义组件,则会去实例化对应的组件(类组件)或执行对应的函数组件(函数组件)。
    3. 元素是“静态结构”,而组件可以包含状态和生命周期,可动态决定元素的结构与内容。

3. Instance(实例)

  • 概念背景

    • 在传统面向对象的思路里,“实例”是通过 new SomeClass() 生成的对象。
    • 在 React 类组件 中,Instance 就是通过 new MyButton(...) 生成的 组件实例对象
    • 在 React 函数组件 中,并不会真正创建传统的“实例对象”,因为函数组件就是一个普通函数。但 React 内部会创建对应的Fiber节点来跟踪该组件的状态与生命周期逻辑。
  • 工作流程

    1. 当 React 遇到 &lt;MyButton /&gt; 时,会判断:
      • 如果 MyButton 是一个类组件,则 React 会调用 new MyButton(props) 来生成一个实例,并调用它的 render() 返回元素。
      • 如果 MyButton 是一个函数组件,则直接执行 MyButton(props),不会有传统面向对象的实例,但依然会有内部 Fiber 结构来记载它的状态、Hooks 等。
    2. 对于 类组件,Instance 负责:
      • 存储组件的状态 this.state
      • 存储对 this.props 的引用
      • 管理组件的生命周期方法(如 componentDidMountcomponentDidUpdate 等)
    3. 对于 函数组件,没有“this”,也没有显式的实例对象,但 React 在内部会创建“Fiber”来保持 Hook 的状态和执行队列。
  • 与 Component/Element 的关系

    1. 组件是一个定义抽象(函数或类),实例是组件类在运行时的一个具体对象
    2. 元素是一个用来描述 UI 的对象,React 在渲染时会根据元素的信息去创建或者更新组件实例(对类组件而言)。
    3. React 通过元素 -> (可能) 实例 -> render() -> 新元素这一流程,不断进行更新和渲染。

4. 三者之间的区别与联系

  1. Component

    • 是什么:定义组件的蓝图或抽象(函数或类)。
    • 作用:编写渲染逻辑(render 方法或者函数返回 JSX)和管理组件的内部状态(对于类组件/Hook)。
    • 关系:只有在渲染时(或者说 React 解析到相应的元素时)才会被真正调用或实例化。
  2. Element

    • 是什么:不可变的对象,描述要渲染的内容和层级结构(“虚拟 DOM”节点)。
    • 作用:告诉 React 如何渲染 UI。由 JSX 或 React.createElement 创建。
    • 关系:组件的 render 或返回值最终都会成为一个或多个 React 元素。React 也会根据这些元素的 type 去决定是否生成/更新对应的 DOM 或组件实例。
  3. Instance

    • 是什么:对类组件来说,Instance 就是 new MyClass() 出来的对象,包含 this.state、生命周期方法等。对函数组件则没有“实例”概念,但有内部的 Fiber 用于管理状态和副作用等。
    • 作用:保存组件的内部状态,处理生命周期回调。
    • 关系:在对元素进行渲染时,如果该元素的 type 是一个类组件,React 就会创建或更新对应的实例;函数组件则不创建传统实例,但依然有抽象的 Fiber 对应组件。

5. 总结与类比

  • Component:类似于类或构造器的定义,本身只是一个静态概念或蓝图。
  • Element:类似于实例化指令,描述了“我想渲染什么组件、什么 DOM、传入什么属性”,形成一个不可变的“UI 配方”
  • Instance:对于类组件,就是真正用 new 关键字生成的对象,包含 this.statethis.props 等所有内部逻辑。对于函数组件,虽没有传统实例,但 React 依然以内部数据结构(Fiber)去跟踪其渲染状态。

从渲染流程上可以这么理解:

  1. 你编写了一个 Component (函数或类)。
  2. JSX 中使用 &lt;MyComponent /&gt; 时,React 会创建一个 Element(即 { type: MyComponent, props: {} } 这样的对象)。
  3. React 根据 Elementtype 判断这是一个函数还是类组件。
    • 如果是类组件,则创建一个 Instancenew MyComponent(props)),再执行 render() 返回新的 Element。
    • 如果是函数组件,则直接调用该函数并返回新的 Element(内部仍会有 Fiber 节点追踪状态)。
  4. 最终根据组件返回的那个 Element 决定下一步渲染或更新 DOM、形成完整 UI 树。

通过以上流程可以看到,Component定义Element渲染描述Instance(或 Fiber)则是实际在内存中管理状态和生命周期的实体。三者在 React 中共同构成了从“编写组件”到“渲染并管理组件”的完整体系。

8. React.createClass 和 extends Component 的区别有哪些?

这道题本质上是在问你对 React 历史演进 的理解。现代 React 项目里,React.createClass 基本只会出现在老代码中;新代码通常是函数组件 + Hooks,老一些的代码会用 class extends React.Component

1. 先说结论

  • React.createClass历史写法,已经不在 React 核心包里。
  • 如果旧项目还在用,通常来自额外依赖 create-react-class
  • class extends React.Component 仍然可用,但在新代码里也已经不是首选。
  • 现在最主流的写法是函数组件。

2. 核心区别

维度React.createClass / createReactClassclass extends React.Component
所处时代早期 React 常见写法ES6 class 时代的主流写法
来源已从 React 核心包移除React 内置支持
this 绑定方法默认自动绑定需要手动 bind 或使用箭头函数
state 初始化getInitialState()constructor 或 class fields
默认 propsgetDefaultProps()defaultProps 或函数参数默认值
使用建议仅维护旧项目时了解老项目里仍然常见,但新代码通常优先函数组件

3. 为什么它容易被问到

因为你一旦在代码里看到下面这些关键词,基本就能判断这是比较老的 React 写法:

  • React.createClass(...)
  • getInitialState()
  • getDefaultProps()
  • 自动绑定 this

4. 现在怎么答更稳妥

可以直接这样概括:

  1. React.createClass 是旧时代 API,现代项目不再推荐使用。
  2. class extends React.Component 比它更现代,也更符合 ES6 语法。
  3. 但从今天的视角看,函数组件 + Hooks 才是默认选项。

5. 一个简短对比例子

旧写法(历史代码里可能见到):

jsx
const createReactClass = require('create-react-class');

const Counter = createReactClass({
  getInitialState() {
    return &#123; count: 0 &#125;;
  },
  handleClick() {
    this.setState(&#123; count: this.state.count + 1 &#125;);
  },
  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;&#123;this.state.count&#125;&lt;/button&gt;;
  },
});

class 写法:

jsx
class Counter extends React.Component {
  state = &#123; count: 0 &#125;;

  handleClick = () =&gt; {
    this.setState((prev) =&gt; (&#123; count: prev.count + 1 &#125;));
  };

  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;&#123;this.state.count&#125;&lt;/button&gt;;
  }
}

6. 小结

  • 面试里要知道 React.createClass 和 class 组件的差异。
  • 实际开发里,React.createClass 只需要当作历史知识点
  • 如果不是在维护旧项目,优先考虑函数组件 + Hooks

9. React 高阶组件是什么,和普通组件有什么区别,适用什么场景

高阶组件(Higher-Order Component,HOC) 是一种基于 React 组件复用的设计模式。

它本质上是一个函数,接受一个组件作为参数,并返回一个经过增强的新组件。


1. 什么是高阶组件

  • 定义
    高阶组件就是一个函数,形如:
jsx
const EnhancedComponent = higherOrderComponent(WrappedComponent);

它接受一个组件 WrappedComponent,并返回一个新的组件 EnhancedComponent

  • 原理
    HOC 通过组合(composition)的方式增强传入的组件,实现代码逻辑的复用,而不修改原有组件的定义。

2. 高阶组件和普通组件的区别

特性普通组件高阶组件 (HOC)
目的用于描述 UI 和交互逻辑用于复用和增强组件的逻辑,通过包装和组合来注入额外的功能
写法通常通过函数组件或 class 组件编写是一个函数,接受组件作为参数返回一个新的组件
职责单一通常关注自身的业务逻辑和 UI 展现关注横切关注点,如权限校验、数据获取、日志记录、状态管理等
复用性一般针对单个业务场景可以将通用功能抽象出来,通过 HOC 应用于多个组件
  • 增强复用
    HOC 可以将多个组件中重复的逻辑提取出来,进行统一处理,避免代码重复。

  • 包装组件
    HOC 会包裹被增强组件,将额外的 props 注入其中,同时也可能拦截或修改生命周期等。


3. 适用场景

3.1 通用逻辑复用

当多个组件中需要使用相同的逻辑时,可以使用 HOC 统一处理,例如:

  • 权限控制:包装组件,判断用户是否有权限访问某些内容。
  • 数据获取:封装网络请求逻辑,自动注入数据和 loading 状态。
  • 日志记录:拦截组件的生命周期、渲染次数等,进行统计或调试。

3.2 组件增强

在不修改原有组件代码的情况下,通过 HOC 为组件增加新的功能,如:

  • 响应式布局:为组件注入屏幕尺寸、滚动位置等数据。
  • 错误边界:包装组件,在出现错误时捕获异常并显示备用 UI。

3.3 状态管理

  • 跨组件共享状态:通过 HOC 来实现状态的共享或注入,使得多个组件之间可以访问同一份数据逻辑。

4. 示例

示例:数据获取 HOC

假设我们有一个 HOC 用于从 API 获取数据,并将数据注入到被包装组件中。

jsx
// withData.js - 定义 HOC
import React from 'react';

function withData(WrappedComponent, url) {
  return class extends React.Component {
    state = {
      data: null,
      loading: true,
      error: null,
    };

    componentDidMount() {
      fetch(url)
        .then(response =&gt; response.json())
        .then(data =&gt; this.setState(&#123; data, loading: false &#125;))
        .catch(error =&gt; this.setState(&#123; error, loading: false &#125;));
    }

    render() {
      // 将状态通过 props 传递给被包装组件
      return &lt;WrappedComponent &#123;...this.props&#125; &#123;...this.state&#125; /&gt;;
    }
  }
}

export default withData;

使用方式:

jsx
// MyComponent.js - 普通组件
import React from 'react';

function MyComponent(&#123; data, loading, error &#125;) {
  if (loading) return &lt;div&gt;Loading...&lt;/div&gt;;
  if (error) return &lt;div&gt;Error!&lt;/div&gt;;
  return (
    &lt;ul&gt;
      {data.map(item =&gt; &lt;li key=&#123;item.id&#125;&gt;&#123;item.name&#125;&lt;/li&gt;)}
    &lt;/ul&gt;
  );
}

export default MyComponent;
jsx
// 使用 HOC 增强组件
import withData from './withData';
import MyComponent from './MyComponent';

const EnhancedMyComponent = withData(MyComponent, 'https://api.example.com/items');

export default EnhancedMyComponent;

在这个例子中,withData HOC 负责处理数据获取的逻辑,而 MyComponent 只专注于如何展示数据。


5. 注意事项

  • 静态属性和引用问题
    使用 HOC 时,需要注意被包装组件的静态方法和属性可能不会自动传递,可以使用工具函数(例如 hoist-non-react-statics)来复制静态属性。

  • 不要滥用
    HOC 是一种强大的抽象,但过度使用可能会使组件层次变得复杂,导致调试和维护困难。在适用场景中使用,能提高代码复用性。

  • 命名约定
    通常在 HOC 内部,使用 WrappedComponent 表示被包装的组件,新组件可以命名为 WithXXXEnhancedComponent 等。


6. 小结

  • 高阶组件(HOC):是一种通过函数包装组件实现复用逻辑和增强功能的设计模式。
  • 区别
    • 普通组件关注具体的 UI 与业务逻辑;
    • HOC 关注横切关注点(如数据获取、权限校验、日志记录等),并通过组合的方式提升代码复用性。
  • 适用场景
    • 当多个组件需要共用同一逻辑或功能时;
    • 需要对组件进行增强而不改变原组件代码时;
    • 管理跨组件状态或行为时。

通过使用高阶组件,我们可以将重复逻辑抽离出来,使得代码更加模块化、易于维护,同时也能为现有组件注入新的功能。

10. 对 componentWillReceiveProps 的理解

componentWillReceiveProps 是一个已经退出主流的旧生命周期。现在更准确的说法是:

  • 它在组件收到新的 props 时触发;
  • 首次挂载时不会调用;
  • 它在现代 React 中已经不推荐继续使用;
  • 如果你在旧项目里看到它,通常应该优先考虑替代方案,而不是继续沿用。

1. 为什么它过时了

这类 componentWill... 生命周期在现代 React 的渲染模型里不够安全,所以后来被迁移为 UNSAFE_componentWillReceiveProps。面试里知道这点即可,实际开发不要再把它当成常规方案。

2. 它以前常被拿来做什么

过去常见的两类用途是:

  • 根据新的 props 同步一份本地 state
  • props 变化时触发副作用,比如请求数据、埋点、重置某些状态。

问题在于,这两类诉求今天通常都有更清晰的写法。

3. 现在推荐怎么替代

场景一:props 变化后要做副作用

类组件用 componentDidUpdate,函数组件用 useEffect

jsx
function UserPanel(&#123; userId &#125;) {
  useEffect(() =&gt; {
    // userId 变化后再请求
    fetchUser(userId);
  }, [userId]);

  return null;
}

场景二:只是根据 props 计算展示结果

优先直接在 render / 函数组件执行过程中计算,不要额外拷贝一份 state。

jsx
function Price(&#123; amount, rate &#125;) {
  const finalPrice = amount * rate;
  return &lt;span&gt;&#123;finalPrice&#125;&lt;/span&gt;;
}

场景三:确实需要从 props 派生 state

这种情况要谨慎。只有当你明确需要“受 props 驱动,但又允许本地修改”时,才考虑 getDerivedStateFromProps 这类方案。

jsx
class Form extends React.Component {
  state = {
    value: this.props.initialValue,
    prevInitialValue: this.props.initialValue,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.initialValue !== state.prevInitialValue) {
      return {
        value: props.initialValue,
        prevInitialValue: props.initialValue,
      };
    }
    return null;
  }

  render() {
    return &lt;input value=&#123;this.state.value&#125; /&gt;;
  }
}

4. 面试里更推荐的答法

可以直接回答:

  1. componentWllReceiveProps 是旧生命周期,新代码不建议使用。
  2. 做副作用时,用 componentDidUpdateuseEffect
  3. 能直接根据 props 计算的内容,就不要再复制到 state
  4. 只有在少量“派生 state”场景下,才考虑 getDerivedStateFromProps

5. 小结

  • componentWillReceiveProps 需要会认,但不需要再会写。
  • 看到旧代码时,优先判断它到底是在做副作用,还是在做派生 state
  • 大多数情况下,componentDidUpdateuseEffect、直接计算、受控组件,都会比它更清晰。

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

React 组件的 render() 方法是 React 更新 UI 的核心部分,以下情况会触发组件的重新渲染:

1. setState() 触发重新渲染

  • 当组件的 state 发生变化时,React 会重新渲染组件。
  • setState() 是异步的,React 会批量更新多个 setState() 调用以提高性能。
  • 例子:
jsx
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = &#123; count: 0 &#125;;
  }

  increment = () =&gt; {
    this.setState(&#123; count: this.state.count + 1 &#125;); // 触发重新渲染
  };

  render() {
    console.log("组件重新渲染");
    return (
      &lt;div&gt;
        &lt;p&gt;计数: &#123;this.state.count&#125;&lt;/p&gt;
        &lt;button onClick=&#123;this.increment&#125;&gt;+1&lt;/button&gt;
      &lt;/div&gt;
    );
  }
}

注意:

  • React 会合并多次 setState() 调用,减少不必要的渲染,提高性能。
  • setState() 可以使用回调形式以确保正确的 prevState
jsx
this.setState((prevState) =&gt; (&#123; count: prevState.count + 1 &#125;));

2. forceUpdate() 触发重新渲染

  • this.forceUpdate() 会强制触发 render() 方法,即使 state 没有改变。
  • 例子:
jsx
class ForceUpdateExample extends React.Component {
  forceRerender = () =&gt; {
    this.forceUpdate(); // 强制重新渲染
  };

  render() {
    console.log("组件重新渲染");
    return &lt;button onClick=&#123;this.forceRerender&#125;&gt;强制刷新&lt;/button&gt;;
  }
}

注意:

  • forceUpdate() 不推荐使用,因为它跳过了 React 的优化机制,可能会导致不必要的渲染。
  • 在大多数情况下,正确更新 state 而不是使用 forceUpdate() 是更好的做法。

3. 父组件重新渲染,子组件也会跟着重新渲染

  • 当父组件 render() 触发时,默认情况下子组件也会重新渲染,即使 props 没有变化。

  • 例子:

jsx
class Parent extends React.Component {
  state = &#123; count: 0 &#125;;

  increment = () =&gt; {
    this.setState(&#123; count: this.state.count + 1 &#125;); // 触发 Parent 重新渲染
  };

  render() {
    console.log("Parent 组件重新渲染");
    return (
      &lt;div&gt;
        &lt;button onClick=&#123;this.increment&#125;&gt;+1&lt;/button&gt;
        &lt;Child name="React" /&gt;
      &lt;/div&gt;
    );
  }
}

function Child(&#123; name &#125;) {
  console.log("Child 组件重新渲染");
  return &lt;p&gt;Hello, &#123;name&#125;&lt;/p&gt;;
}

输出日志

Parent 组件重新渲染
Child 组件重新渲染
  • 即使 Child 组件的 props 没有改变,它仍然会重新渲染,因为 Parent 重新渲染时,默认情况下所有子组件都会重新渲染。

优化方法:使用 React.memo

  • 如果子组件 props 没有变化,我们可以用 React.memo() 让它跳过不必要的渲染:
jsx
const Child = React.memo(function Child(&#123; name &#125;) {
  console.log("Child 组件重新渲染");
  return &lt;p&gt;Hello, &#123;name&#125;&lt;/p&gt;;
});

这样,Child 组件只会在 props 变化时重新渲染,而不是每次 Parent 重新渲染时都更新。

4. useState()useReducer() 触发重新渲染

  • 函数组件 中,调用 useStatesetState() 方法或者 useReducerdispatch 方法都会导致组件重新渲染。
  • 示例(useState):
jsx
function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    &lt;div&gt;
      &lt;p&gt;Count: &#123;count&#125;&lt;/p&gt;
      &lt;button onClick=&#123;() =&gt; setCount(count + 1)&#125;&gt;+1&lt;/button&gt;
    &lt;/div&gt;
  );
}
  • 示例(useReducer):
    jsx
    function reducer(state, action) {
      switch (action.type) {
        case "increment":
          return &#123; count: state.count + 1 &#125;;
        default:
          return state;
      }
    }
    
    function Counter() {
      const [state, dispatch] = React.useReducer(reducer, &#123; count: 0 &#125;);
    
      return (
        &lt;div&gt;
          &lt;p&gt;Count: &#123;state.count&#125;&lt;/p&gt;
          &lt;button onClick={() =&gt; dispatch(&#123; type: "increment" &#125;)}&gt;+1&lt;/button&gt;
        &lt;/div&gt;
      );
    }
    注意:
    • setState/dispatch 不会立即生效,React 会批量合并多次状态更新以优化性能。
    • 相同值不会触发重新渲染(即 setCount(0) 不会触发 render(),因为 count 没有变化)。

React 重新渲染时,render() 方法做了什么?

当组件重新渲染时,React 会执行以下步骤:

  1. 调用 render() 或 函数组件重新执行
    • React 会调用组件的 render() 方法(类组件)或重新执行整个函数组件(函数组件)。
    • 示例(类组件)
jsx
class MyComponent extends React.Component {
  render() {
  }
}
  • 示例(函数组件)
jsx
function MyComponent() {
  console.log("组件重新渲染");
  return &lt;p&gt;Hello, World!&lt;/p&gt;;
}
  • 每次 setState()props 变化时,render() 都会被调用
  1. 比较虚拟 DOM(Virtual DOM)
    • React 使用 Diffing AlgorithmReact Reconciliation),比较新的 Virtual DOM 和之前的 Virtual DOM,找出哪些部分发生了变化。
  2. 计算最小更新
    • React 不会直接更新整个 DOM,而是最小化 DOM 更新,只更新变化的部分
    • 例如:
jsx
&lt;div&gt;
  &lt;p&gt;原内容&lt;/p&gt;
&lt;/div&gt;

更新后:

jsx
&lt;div&gt;
  &lt;p&gt;新内容&lt;/p&gt; &lt;!-- 只更新这个 &lt;p&gt; 标签 --&gt;
&lt;/div&gt;
  1. 更新真实 DOM
    • React 使用 批量更新(Batch Updates) 来提高性能,而不是每次 setState() 变化都立即操作 DOM。

小结

✅ 可以触发 React 重新渲染的方法

  1. this.setState()(类组件)
  2. forceUpdate()(强制更新,不推荐)
  3. useState() / useReducer()(函数组件)
  4. 父组件 render() 时,子组件默认也会重新渲染(可用 React.memo 优化)
  5. useContext() 依赖的 value 变化

✅ React 重新渲染时 render() 做了什么?

  1. 重新执行 render()(类组件)或重新执行函数组件
  2. 比较新的 Virtual DOM 和旧的 Virtual DOM
  3. 计算最小 DOM 更新
  4. 高效更新真实 DOM

优化技巧:

  • 避免不必要的 setState()
  • 使用 React.memo 避免子组件重复渲染
  • useCallback() & useMemo() 优化函数组件的性能
  • 使用 不可变数据(Immutable Data) 避免对象引用不变但内容变化时的无效渲染

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

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后React决定是否应该重新渲染组件。只要组件的state发生变化,React就会对组件进行重新渲染。这是因为React中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。

当React将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉React什么时候重新渲染什么时候跳过重新渲染。

13. React 声明组件有哪几种方法,有什么不同?

从今天的 React 视角看,可以分成 3 类:

  1. 函数组件:当前主流写法。
  2. class 组件:仍然支持,但主要出现在老项目里。
  3. React.createClass / create-react-class:历史写法,只在更老的代码里出现。

1. 函数组件

函数组件已经不等于“无状态组件”。有了 Hooks 之后,函数组件同样可以拥有:

  • stateuseState / useReducer
  • 副作用(useEffect
  • refuseRef
  • Context(useContext

所以今天如果面试官问“怎么声明组件”,第一反应应该是函数组件

2. class 组件

class 组件的特点是:

  • 通过 extends React.Component 定义;
  • 可以使用 this.state、生命周期方法、实例方法;
  • 方法默认不会自动绑定 this
  • 在维护旧项目、编写 Error Boundary 等场景里仍然会见到。

3. React.createClass

这是更老的写法,特点是:

  • 不在 React 核心包中;
  • getInitialState()getDefaultProps() 这类老式 API;
  • 会自动绑定 this
  • 现在更多是“看懂旧代码”的知识点,而不是“继续使用”的推荐方案。

4. 一句话对比

  • 函数组件:现代 React 默认选项。
  • class 组件:兼容旧代码的常见形态。
  • React.createClass:历史遗留写法。

14. 对有状态组件和无状态组件的理解及使用场景

这道题容易答成旧版本口径。现在更准确的说法是:

  • “有状态 / 无状态” 是按组件是否维护状态来分;
  • “函数组件 / class 组件” 是按组件的声明方式来分;
  • 这两组概念不能直接画等号

1. 什么是有状态组件

只要组件内部维护了会影响渲染的数据,都可以认为它是有状态组件。例如:

  • class 组件里的 this.state
  • 函数组件里的 useState
  • 函数组件里的 useReducer

使用场景:

  • 组件需要响应用户输入;
  • 组件需要维护本地 UI 状态,如展开/收起、分页、表单草稿;
  • 组件需要根据交互不断更新界面。

2. 什么是无状态组件

无状态组件通常只接收 props,根据输入直接渲染 UI,本身不维护业务状态。

典型场景:

  • 展示型组件
  • 布局组件
  • 纯样式封装组件
  • 只做数据格式化展示的组件

3. 今天最容易说错的点

  • 函数组件不再是“无状态组件”,因为 Hooks 让它也能管理状态。
  • 函数组件可以使用 ref,也可以有副作用。
  • 无状态组件不一定性能更高,关键还是看组件边界、数据流和是否发生了不必要的重渲染。

4. 面试里怎么总结

可以直接回答:

  1. 有状态 / 无状态描述的是“是否维护状态”。
  2. 函数组件 / class 组件描述的是“如何声明组件”。
  3. 在现代 React 里,函数组件既可以是无状态组件,也可以是有状态组件。

15. 对React中Fragment的理解,它的使用场景是什么?

1. 什么是 React.Fragment

React.Fragment 是 React 提供的一种特殊组件,用于在不增加额外 DOM 结构的情况下,返回多个元素

在 React 组件中,render() 方法(或函数组件)只能返回一个根元素。但有时候,我们需要返回多个 JSX 元素,而不想在 DOM 结构中添加额外的 &lt;div&gt; 包裹层,这时就可以使用 React.Fragment


2. 为什么需要 React.Fragment

在 React 早期版本中,如果想要在 render() 方法返回多个元素,通常会使用 &lt;div&gt; 包裹所有子元素,例如:

jsx
function MyComponent() {
  return (
    &lt;div&gt;
      &lt;h1&gt;标题&lt;/h1&gt;
      &lt;p&gt;这是一个段落&lt;/p&gt;
    &lt;/div&gt;
  );
}

虽然这样可以正常工作,但它会在 DOM 结构中增加额外的 &lt;div&gt;,可能会影响布局或样式,例如:

html
&lt;div&gt;  &lt;!-- 额外的 div --&gt;
  &lt;h1&gt;标题&lt;/h1&gt;
  &lt;p&gt;这是一个段落&lt;/p&gt;
&lt;/div&gt;

如果在一些严格的 HTML 结构中,比如 &lt;ul&gt;&lt;table&gt;,额外的 &lt;div&gt; 可能会导致语法错误或影响样式。


3. 使用 React.Fragment 解决问题

使用 React.Fragment,可以避免额外的 &lt;div&gt;,让 JSX 直接返回多个元素,而不影响 DOM 结构:

jsx
function MyComponent() {
  return (
    &lt;React.Fragment&gt;
      &lt;h1&gt;标题&lt;/h1&gt;
      &lt;p&gt;这是一个段落&lt;/p&gt;
    &lt;/React.Fragment&gt;
  );
}

最终生成的 DOM 结构:

html
&lt;h1&gt;标题&lt;/h1&gt;
&lt;p&gt;这是一个段落&lt;/p&gt;

这样就不会有额外的 &lt;div&gt; 影响布局。


4. React.Fragment 的简写形式

React 提供了简写语法&lt;&gt;...&lt;/&gt;,可以减少代码量:

jsx
function MyComponent() {
  return (
    &lt;&gt;
      &lt;h1&gt;标题&lt;/h1&gt;
      &lt;p&gt;这是一个段落&lt;/p&gt;
    &lt;/&gt;
  );
}

这个 &lt;&gt;...&lt;/&gt; 语法与 React.Fragment 完全等效,但更简洁。


5. React.Fragment 的使用场景

(1) 避免不必要的 DOM 结构

有时,多个组件需要被一起返回,但不希望增加额外的 div,这时 Fragment 很有用:

jsx
function ListItems() {
  return (
    &lt;React.Fragment&gt;
      &lt;li&gt;Item 1&lt;/li&gt;
      &lt;li&gt;Item 2&lt;/li&gt;
      &lt;li&gt;Item 3&lt;/li&gt;
    &lt;/React.Fragment&gt;
  );
}

function MyList() {
  return (
    &lt;ul&gt;
      &lt;ListItems /&gt;
    &lt;/ul&gt;
  );
}

如果用 &lt;div&gt; 包裹 li,会导致 HTML 结构不合法:

html
&lt;ul&gt;
  &lt;div&gt; &lt;!-- ❌ 错误:&lt;div&gt; 不能直接包裹 &lt;li&gt; --&gt;
    &lt;li&gt;Item 1&lt;/li&gt;
    &lt;li&gt;Item 2&lt;/li&gt;
    &lt;li&gt;Item 3&lt;/li&gt;
  &lt;/div&gt;
&lt;/ul&gt;

使用 React.Fragment 后:

html
&lt;ul&gt;
  &lt;li&gt;Item 1&lt;/li&gt;
  &lt;li&gt;Item 2&lt;/li&gt;
  &lt;li&gt;Item 3&lt;/li&gt;
&lt;/ul&gt;

不会影响 ul 的正确性


(2) 在 table 结构中避免额外 DOM 影响布局

HTML 中,tabletheadtbodytr 结构必须严格遵守规范,不能随意插入 div,否则可能导致样式或布局问题:

jsx
function TableRow() {
  return (
    &lt;&gt;
      &lt;tr&gt;
        &lt;td&gt;张三&lt;/td&gt;
        &lt;td&gt;28&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;李四&lt;/td&gt;
        &lt;td&gt;30&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/&gt;
  );
}

function MyTable() {
  return (
    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          &lt;th&gt;姓名&lt;/th&gt;
          &lt;th&gt;年龄&lt;/th&gt;
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        &lt;TableRow /&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  );
}

如果这里用 &lt;div&gt; 包裹 &lt;tr&gt;,浏览器可能会自动修正错误的 HTML 结构,导致意外的布局问题。而 Fragment 允许你返回多个 tr,不会影响 table 结构。


(3) 提高渲染性能

React.Fragment &lt;div&gt; 具备更好的性能

  • 减少 DOM 节点的数量,提高渲染性能,减少不必要的 DOM 计算。
  • 特别适合 React 组件树深度较深的场景,避免不必要的嵌套结构。

6. React.Fragmentkey 属性

map() 渲染列表时,通常需要 key 标识唯一性,但 &lt;&gt;...&lt;/&gt; 语法不支持 key,需要用 React.Fragment

jsx
function List() {
  const items = ["苹果", "香蕉", "橙子"];

  return (
    &lt;React.Fragment&gt;
      {items.map((item, index) =&gt; (
        &lt;React.Fragment key=&#123;index&#125;&gt;
          &lt;p&gt;&#123;item&#125;&lt;/p&gt;
        &lt;/React.Fragment&gt;
      ))}
    &lt;/React.Fragment&gt;
  );
}

如果直接用 &lt;&gt;...&lt;/&gt;

jsx
{items.map((item, index) =&gt; (
  &lt;&gt;  // ❌ 不能在这里使用 key
    &lt;p&gt;&#123;item&#125;&lt;/p&gt;
  &lt;/&gt;
))}

使用 React.Fragment 显式写出 key,可以避免 React 的 key 警告。


7. 什么时候不用 React.Fragment

  • 只有一个根元素时:如果 render() 只返回单个元素,则 不需要 Fragment

    jsx
    function MyComponent() {
      return &lt;h1&gt;Hello World&lt;/h1&gt;; // 不需要 Fragment
    }
  • 需要添加 classNamestyledata-* 属性时

    • React.Fragment 不能像 &lt;div&gt; 一样使用 classNameid

      jsx
      &lt;React.Fragment className="box"&gt; // ❌ 无效,Fragment 不支持 className
        &lt;h1&gt;Hello&lt;/h1&gt;
        &lt;p&gt;World&lt;/p&gt;
      &lt;/React.Fragment&gt;
    • 解决方案:使用 div&lt;span&gt;,如果一定要用 Fragment,只能用 key


8. React.Fragment vs &lt;&gt;...&lt;/&gt; 的区别

React.Fragment&lt;&gt;...&lt;/&gt; (短语法)
是否支持 key✅ 是❌ 否
是否支持 className❌ 否❌ 否
是否支持 style❌ 否❌ 否
语法简洁性需要写 React.Fragment✅ 更简洁
适用场景适用于 map() 需要 key适用于简单 JSX 结构

一般建议

  • 如果需要 key,用 React.Fragment
  • 如果不需要 key,用 &lt;&gt;...&lt;/&gt; 更简洁。

9. 总结

  • React.Fragment 允许在 render() 方法返回多个元素,不会生成额外的 DOM 结构。
  • 适用于:
    • 避免 div 影响布局。
    • tableul 等严格的 HTML 结构。
    • 提高渲染性能,减少 DOM 计算。
  • 短语法 &lt;&gt;...&lt;/&gt; 更简洁,但不能使用 key
  • 最佳实践:如果 map() 需要 key,用 React.Fragment;其他情况用 &lt;&gt;...&lt;/&gt;

🚀 这样可以让你的 React 代码更清晰、性能更高!

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

在 React 中,可以通过 ref(引用) 获取组件对应的 DOM 元素。React 提供了多种方式来获取 DOM,具体方法如下:


1. 使用 useRef(推荐方式,适用于函数组件)

函数组件 中,我们可以使用 useRef 获取 DOM 元素。

示例:获取 input DOM

jsx
import React, &#123; useRef &#125; from "react";

function InputFocus() {
  const inputRef = useRef(null); // 创建 ref

  const focusInput = () =&gt; {
    inputRef.current.focus(); // 获取 DOM 元素并聚焦
  };

  return (
    &lt;div&gt;
      &lt;input ref=&#123;inputRef&#125; type="text" placeholder="输入点什么..." /&gt;
      &lt;button onClick=&#123;focusInput&#125;&gt;点击聚焦输入框&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default InputFocus;

解释

  • useRef(null) 创建一个 ref 对象
  • ref 绑定到 input 元素上 (ref=&#123;inputRef&#125;)。
  • inputRef.current 指向真实的 DOM 元素
  • inputRef.current.focus() 调用 focus() 方法,使 input 聚焦

适用于

  • 函数组件(推荐)。
  • 获取 DOM 元素(input, div, button 等)
  • 管理 DOM 交互(focus, scroll, class 操作)

2. 使用 createRef(适用于类组件)

类组件 中,React 提供 React.createRef() 作为获取 DOM 的方法。

示例:类组件获取 DOM

jsx
import React, &#123; Component, createRef &#125; from "react";

class InputFocus extends Component {
  constructor(props) {
    super(props);
    this.inputRef = createRef(); // 创建 ref
  }

  focusInput = () =&gt; {
    this.inputRef.current.focus(); // 获取 DOM 并聚焦
  };

  render() {
    return (
      &lt;div&gt;
        &lt;input ref=&#123;this.inputRef&#125; type="text" placeholder="输入点什么..." /&gt;
        &lt;button onClick=&#123;this.focusInput&#125;&gt;点击聚焦输入框&lt;/button&gt;
      &lt;/div&gt;
    );
  }
}

export default InputFocus;

解释

  • this.inputRef = React.createRef(); 创建 ref
  • &lt;input ref=&#123;this.inputRef&#125; /&gt; 绑定到 DOM
  • this.inputRef.current.focus(); 操作 DOM

适用于

  • 类组件(推荐)。
  • 获取 DOM 节点

3. callback ref(函数形式)

另一种方式是使用回调 refcallback ref),可以直接操作 ref 而不使用 useRefcreateRef

示例:回调 ref 获取 input

jsx
class InputFocus extends React.Component {
  focusInput = () =&gt; {
    this.inputRef.focus(); // 直接访问 DOM
  };

  render() {
    return (
      &lt;div&gt;
        &lt;input ref=&#123;(el) =&gt; (this.inputRef = el)&#125; type="text" /&gt;
        &lt;button onClick=&#123;this.focusInput&#125;&gt;点击聚焦输入框&lt;/button&gt;
      &lt;/div&gt;
    );
  }
}

解释

  • ref=&#123;(el) =&gt; (this.inputRef = el)&#125; 直接把 DOM 赋值给 this.inputRef
  • this.inputRef.focus() 直接操作 DOM。

适用于

  • ref 需要动态变化时(例如:条件渲染)。
  • 适用于类组件

4. 获取组件暴露的 DOM(forwardRef / ref prop)

这部分需要分 React 版本来理解:

  • React 18 及更早版本:如果父组件想拿到自定义函数组件内部的 DOM,通常要用 forwardRef
  • React 19+ref 可以像普通 prop 一样传入组件,forwardRef 不再是首选写法。

React 18 及更早版本:forwardRef

jsx
import React, &#123; forwardRef, useRef &#125; from "react";

const MyInput = forwardRef((props, ref) =&gt; {
  return &lt;input ref=&#123;ref&#125; type="text" placeholder="输入..." /&gt;;
});

function Parent() {
  const inputRef = useRef(null);

  return (
    &lt;div&gt;
      &lt;MyInput ref=&#123;inputRef&#125; /&gt;
      &lt;button onClick=&#123;() =&gt; inputRef.current.focus()&#125;&gt;聚焦&lt;/button&gt;
    &lt;/div&gt;
  );
}

React 19+:直接接收 ref

jsx
import &#123; useRef &#125; from "react";

function MyInput(&#123; ref, ...props &#125;) {
  return &lt;input ref=&#123;ref&#125; &#123;...props&#125; /&gt;;
}

function Parent() {
  const inputRef = useRef(null);

  return (
    &lt;div&gt;
      &lt;MyInput ref=&#123;inputRef&#125; placeholder="输入..." /&gt;
      &lt;button onClick=&#123;() =&gt; inputRef.current.focus()&#125;&gt;聚焦&lt;/button&gt;
    &lt;/div&gt;
  );
}

适用于

  • 父组件需要获取子组件内部的 DOM
  • 组件库需要向外暴露 DOM 句柄
  • 兼容 React 18 老代码时继续使用 forwardRef

5. useImperativeHandle(自定义 ref

有时候,我们希望对子组件暴露更具体的 API,可以用 useImperativeHandle 自定义 ref

示例:父组件调用子组件的方法

jsx
import React, &#123; useRef, useImperativeHandle, forwardRef &#125; from "react";

// 子组件
const MyInput = forwardRef((props, ref) =&gt; {
  const inputRef = useRef();

  useImperativeHandle(ref, () =&gt; ({
    focus: () =&gt; inputRef.current.focus(), // 自定义暴露的方法
    clear: () =&gt; (inputRef.current.value = ""), // 自定义清除 input
  }));

  return &lt;input ref=&#123;inputRef&#125; type="text" /&gt;;
});

// 父组件
function Parent() {
  const inputRef = useRef();

  return (
    &lt;div&gt;
      &lt;MyInput ref=&#123;inputRef&#125; /&gt;
      &lt;button onClick=&#123;() =&gt; inputRef.current.focus()&#125;&gt;聚焦&lt;/button&gt;
      &lt;button onClick=&#123;() =&gt; inputRef.current.clear()&#125;&gt;清空&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default Parent;

解释

  • useImperativeHandle(ref, () =&gt; (&#123; focus, clear &#125;)) 自定义暴露给父组件的方法。
  • 父组件可以 inputRef.current.focus()inputRef.current.clear() 调用子组件的方法。

适用于

  • 希望子组件只暴露特定的方法,而不是整个 DOM。

6. document.querySelector(直接查找 DOM,不推荐)

虽然可以使用 document.querySelector() 直接查找 DOM,但在 React 中不推荐这样做,因为它绕过了 React 的虚拟 DOM 机制,可能导致不一致的 UI。

jsx
function MyComponent() {
  const handleClick = () =&gt; {
    document.querySelector("input").focus(); // 获取 DOM
  };

  return (
    &lt;div&gt;
      &lt;input type="text" /&gt;
      &lt;button onClick=&#123;handleClick&#125;&gt;点击聚焦&lt;/button&gt;
    &lt;/div&gt;
  );
}

问题:

  • 直接操作 DOM 可能导致 React 的状态失效。
  • 在动态渲染组件时可能导致 querySelector 找不到元素。

推荐使用 ref 代替 querySelector


7. 总结

方法适用组件适用场景推荐程度
useRef()函数组件访问 DOM、保存不参与渲染的可变值✅✅✅
createRef()类组件在 class 组件中访问 DOM✅✅
回调 ref类组件 / 特殊场景ref 目标会动态切换时
forwardRef()自定义组件React 18 及更早版本里透传 ref✅✅
ref 作为 prop自定义组件React 19+ 中透传 ref✅✅✅
useImperativeHandle()函数组件只暴露受控的方法,而不是整个 DOM✅✅✅
document.querySelector()任何组件直接查找 DOM

最佳实践

  • 函数组件获取 DOM:优先 useRef()
  • class 组件获取 DOM:使用 createRef()
  • 子组件需要暴露 DOM 或句柄:React 18 用 forwardRef(),React 19+ 可以直接接收 ref
  • 只想暴露有限 API:配合 useImperativeHandle()

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

更准确的说法是:不应该在 render 过程中读取或写入 ref.current(初始化时的个别安全写法除外)。

原因有 3 个:

  1. render / 函数组件主体应该保持纯净,不应该依赖会变动的外部可变值。
  2. 首次渲染时 DOM 还没挂载完成,这时 ref.current 往往还是 null
  3. ref 的变化不会触发重新渲染,所以如果某个值会影响 UI,应该放进 state,而不是依赖 ref

更合适的读取位置是:

  • 事件处理函数里;
  • useEffect / useLayoutEffect 里;
  • class 组件的 componentDidMount / componentDidUpdate 里。

18. 对React的插槽(Portals)的理解,如何使用,有哪些使用场景

React Portals(插槽)是一种在 DOM 结构上“跳出”父组件层级限制,将组件的子节点渲染到指定 DOM 节点的技术。下面详细解释它的概念、使用方法和适用场景:

1. 概念理解

  • 什么是 Portals?
    Portals 允许你将子组件挂载到 DOM 树中一个与父组件层级无关的位置。这意味着,即使组件逻辑和状态管理都在父组件中,但它们的 DOM 结构可以放在页面的其他位置。

  • 事件冒泡
    值得注意的是,尽管 Portals 使得 DOM 结构发生了改变,但 React 依然会保持事件的冒泡行为。也就是说,事件仍然会从 Portal 内部的元素向上冒泡到父组件。

2. 如何使用 Portals

使用 Portals 非常简单,只需要使用 ReactDOM.createPortal 方法。其基本用法如下:

jsx
import React from 'react';
import ReactDOM from 'react-dom';

class Modal extends React.Component {
  render() {
    // 假设页面中有一个 id 为 'modal-root' 的 DOM 节点
    return ReactDOM.createPortal(
      &lt;div className="modal"&gt;
        &#123;this.props.children&#125;
      &lt;/div&gt;,
      document.getElementById('modal-root')
    );
  }
}

在上面的代码中:

  • 第一个参数 是要渲染的 React 元素(比如模态框的内容)。
  • 第二个参数 是目标 DOM 节点,该节点可以在整个应用之外,例如在 index.html 中单独定义一个 &lt;div id="modal-root"&gt;&lt;/div&gt;

3. 使用场景

Portals 的特性使其在以下场景中非常有用:

  • 模态框/对话框
    模态框通常需要在页面的最外层展示,以避免被父容器的 CSS 属性(如 overflow: hidden)遮挡。通过 Portals,可以确保模态框不受父组件布局的限制。

  • 通知/提示信息
    比如全局提示、Toast 通知等,这些通常需要在页面的固定位置展示,而不受组件嵌套的影响。

  • 工具提示(Tooltip)和下拉菜单
    当这些组件需要脱离正常的 DOM 流布局以避免遮挡或溢出问题时,可以利用 Portals 渲染到 body 或其他特定的容器中。

  • 全局加载器或遮罩层
    当需要全屏展示一个加载状态或遮罩层时,使用 Portals 可以确保其覆盖整个页面,而不是局限于某个父容器的范围。

总结

React Portals 让我们可以在不破坏组件逻辑层次的前提下,灵活地控制 DOM 渲染位置,非常适合处理模态框、通知、工具提示等需要全局覆盖或者脱离父组件限制的场景。通过 ReactDOM.createPortal 方法,可以轻松将组件内容挂载到任意指定的 DOM 节点,同时保持事件系统的连贯性。

19. 在 React 中如何避免不必要的 render?

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:

下面列举几种常用方法:

  1. 使用 PureComponent 或 React.memo

    • PureComponent:对于类组件,可以继承 React.PureComponent,它会自动进行浅比较(shallow compare),从而只在 props 或 state 发生改变时才触发 render。
    • React.memo:对于函数组件,可以使用 React.memo 包装组件,类似于 PureComponent,它会对组件的 props 进行浅比较,从而避免不必要的渲染。
  2. 合理使用 shouldComponentUpdate
    对于类组件,可以手动实现 shouldComponentUpdate 方法,根据实际需求精细控制组件是否需要更新。这样可以避免一些因父组件更新而引起的无意义渲染。

  3. 使用 useMemo 和 useCallback

    • useMemo:用来缓存计算结果,只有在依赖变化时才重新计算,从而减少不必要的重新计算,避免传递新的引用给子组件。
    • useCallback:用来缓存函数引用,确保在组件重新渲染时不创建新的函数对象,防止子组件因函数引用改变而重新渲染。
  4. 拆分组件和提升状态

    • 拆分组件:将复杂组件拆分为多个小组件,确保只有状态或 props 变化的部分才会重新渲染。
    • 提升状态:合理规划状态的分布,避免不必要的全局状态更新导致整个组件树的重新渲染。
  5. 避免不必要的状态更新

    • 确保只有在真正需要更新时才调用 setState 或更新 state,避免在 render 中直接进行状态修改。
    • 使用不可变数据结构,确保通过浅比较即可判断数据是否发生了变化。
  6. 合适的 key 使用
    在列表渲染时,确保使用唯一且稳定的 key 值,帮助 React 更好地复用组件,避免因重新渲染而带来的性能问题。

通过以上方法,可以有效减少 React 中不必要的渲染,提高应用性能。

20. 对 React-Intl 的理解,它的工作原理?

React-intl是雅虎的语言国际化开源项目FormatJS的一部分,通过其提供的组件和API可以与ReactJS绑定。

React-intl提供了两种使用方法,一种是引用React组件,另一种是直接调取API,官方更加推荐在React项目中使用前者,只有在无法使用React组件的地方,才应该调用框架提供的API。它提供了一系列的React组件,包括数字格式化、字符串格式化、日期格式化等。

在React-intl中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。

21. 对 React context 的理解

在 React 中,数据默认通过 props 自上而下传递,这种单向数据流简单、直接、可预测。但当数据需要跨很多层组件传递时,就容易出现 props drilling

Context 的作用就是:让某个组件树范围内的数据可以被深层组件直接读取,而不必层层手动透传 props

更适合这样理解:

  • props 适合明确、局部的数据传递;
  • Context 适合“很多层都要用,但又不想每层都手传”的共享数据;
  • 常见场景包括:主题、语言、当前登录用户、路由层级、表单上下文。

需要注意的是,现代 React 的 Context 指的是 createContext + Provider + useContext 这一套能力,而不是早期的 getChildContext 旧 API。

22. 为什么不应该把 Context 当作默认方案?

这道题的现代口径不是“Context 不推荐用”,而是:

> Context 很有用,但不适合被当成默认的全局状态方案。

原因主要有 3 个:

  1. 它适合共享语义明确的数据,不适合什么都往里塞。 比如主题、语言、当前用户很适合;但如果把大量高频变化的业务状态都塞进一个 Context,会让依赖它的子树频繁更新。

  2. 它解决的是“跨层传递”,不是完整状态管理。 Context 本身不负责异步流程、缓存策略、复杂更新逻辑、时间旅行调试等能力。

  3. 先用简单方式表达数据流通常更清晰。 如果只是父子或两三层传值,props 往往更直接;只有当 props drilling 明显影响可读性时,再考虑 Context。

所以更稳妥的结论是:

  • 不是不用 Context,而是不要滥用 Context。
  • 先看 props / 组件组合能不能解决。
  • 真有跨层共享需求时再用 Context。
  • 如果状态复杂、更新频繁,再考虑拆分多个 Context、useReducer,或引入专门的状态管理方案。

23. React 中什么是受控组件和非控组件?

(1)受控组件 在使用表单来收集用户输入时,例如&lt;input&gt;&lt;select&gt;&lt;textearea&gt;等元素都要绑定一个change事件,当表单的状态发生变化,就会触发onChange事件,更新组件的state。这种组件在React中被称为受控组件,在受控组件中,组件渲染出的状态与它的value或checked属性相对应,react通过这种方式消除了组件的局部状态,使整个状态可控。react官方推荐使用受控表单组件。

受控组件更新state的流程:

  • 可以通过初始state中设置表单的默认值
  • 每当表单的值发生变化时,调用onChange事件处理器
  • 事件处理器通过事件对象e拿到改变后的状态,并更新组件的state
  • 一旦通过setState方法更新state,就会触发视图的重新渲染,完成表单组件的更新

受控组件缺陷: 表单元素的值都是由React组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。

(2)非受控组件 如果一个表单组件没有value props(单选和复选按钮对应的是checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个ref来从DOM获得表单值。而不是为每个状态更新编写一个事件处理程序。

React官方的解释:

> 要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref来从 DOM 节点中获取表单数据。 因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

24. React 中 refs 的作用是什么?有哪些应用场景?

ref 的核心作用有两类:

  1. 拿到 DOM 或子组件暴露出来的句柄
  2. 保存一个不会触发重新渲染的可变值

常见应用场景:

  • 输入框聚焦、滚动定位、文本选择;
  • 播放/暂停媒体;
  • 与第三方 DOM 库集成;
  • 保存定时器 ID、上一次值、外部实例等不参与渲染的数据。

现代 React 里常见写法是:

  • 函数组件useRef()
  • class 组件createRef()
  • 自定义组件对外暴露 DOM/方法
    • React 18 及更早版本常用 forwardRef
    • React 19+ 可以直接把 ref 当作 prop 接收

需要注意:

  • ref 不是状态管理工具,会影响渲染的值应该放进 state
  • 不要为了省事把正常的数据流都改成 ref
  • 不要在 render 过程中读写 ref.current

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

构造函数只针对 class 组件 而言,而且不是必须的

它最常见的两个用途是:

  • 初始化 this.state
  • 绑定实例方法里的 this

示例:

jsx
class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = &#123; liked: false &#125;;
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState((prev) =&gt; (&#123; liked: !prev.liked &#125;));
  }

  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;&#123;String(this.state.liked)&#125;&lt;/button&gt;;
  }
}

但放到今天来看,有两个补充很重要:

  • 如果你用的是 class fields,很多时候连构造函数都不需要写。
  • 新项目里通常优先使用函数组件 + Hooks,构造函数出现得越来越少。

例如,下面这个 class 组件就不需要 constructor

jsx
class LikeButton extends React.Component {
  state = &#123; liked: false &#125;;

  handleClick = () =&gt; {
    this.setState((prev) =&gt; (&#123; liked: !prev.liked &#125;));
  };

  render() {
    return &lt;button onClick=&#123;this.handleClick&#125;&gt;&#123;String(this.state.liked)&#125;&lt;/button&gt;;
  }
}

注意点:

  • 只有写了 constructor,才需要在里面调用 super(props)
  • 不要在 constructor 里做副作用、请求、订阅;
  • 如果只是写新代码,优先考虑函数组件,不必纠结构造函数。

27. React.forwardRef 是什么?它有什么作用?

React.forwardRef 的作用是:让组件把收到的 ref 转发给内部的 DOM 节点或自定义句柄。

典型用途有两个:

  • 父组件需要拿到子组件内部的 DOM;
  • 高阶组件或组件库封装后,仍然希望把 ref 继续透传出去。

需要注意版本语境:

  • React 18 及更早版本forwardRef 是函数组件透传 ref 的常见方案。
  • React 19+ref 可以直接作为 prop 传入组件,所以 forwardRef 更多是为了兼容旧写法。

如果只是想暴露有限的方法,而不是整个 DOM,通常会配合 useImperativeHandle 一起使用。

28. 类组件与函数组件有什么异同?

相同点: 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。

我们甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。所以,基本可认为两者作为组件是完全一致的。

不同点:

  • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
  • 早期很多场景必须依赖类组件的生命周期;但现在有了 Hooks 之后,大多数日常业务组件都可以用函数组件完成。类组件依然支持,只是在新代码里不再是默认选择。
  • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
  • 从今天的生态来看,函数组件已经是主流方案;类组件更多出现在旧项目维护、少量边界场景,或面试对比题里。
  • 在并发渲染、逻辑复用和代码组织上,函数组件通常更自然;类组件则因为生命周期和实例模型,理解与维护成本往往更高。

评论区

欢迎留言、补充或勘误。

xiaoba.blog