Interview 2025
首页
React 面试
首页
React 面试
  • React 国内国外大厂面试30问(含答案整理)

React 国内国外大厂面试30问(含答案整理)

一、API高频考题

1. 高阶组件(HOC)是什么?你在业务中使用过解决了什么问题?

答案:

HOC本质上是一个函数,它接收一个组件作为参数,然后返回一个新的组件。返回的新组件将拥有被包裹的组件的所有props,并且可以添加额外的props或状态。HOC可以用于抽象出组件之间的共享代码,以增强组件的复用性和可维护性,也可以用于控制props、封装组件状态,或者通过引用(ref)来访问组件实例。

示例代码:

function withExtraProp(Component) {
  return function(props) {
    return <Component extraProp="someValue" {...props} />;
  };
}

业务中常见使用场景:

  1. 授权和权限管理:创建HOC检查用户是否已认证,仅允许认证用户访问组件。
  2. 数据获取:HOC在挂载时获取数据,并通过props传递给被包裹组件。
  3. 错误处理:HOC包裹原组件渲染,发生错误时显示错误信息或备用内容。

2. 什么时候应该使用类组件而不是函数组件?React组件错误捕获怎么做?

答案:

  • 类组件vs函数组件:

    • 早期React版本中,类组件提供生命周期方法(如componentDidMount、componentDidUpdate)和state,函数组件无状态且无生命周期方法,需维护状态或使用生命周期时需用类组件。
    • React 16.8引入Hooks后,函数组件可通过useState使用状态、useEffect使用生命周期特性,几乎可替代所有类组件场景。
    • 唯一必须使用类组件的场景:实现错误边界(Error Boundaries),错误边界只能通过类组件实现。
  • React组件错误捕获(错误边界实现): 错误边界允许在子组件树中捕获JavaScript错误,显示备用内容而非整个组件树崩溃。

示例代码:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 发生错误时,更新状态使下一次渲染显示备用UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 可将错误日志上报给错误报告服务
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>; // 自定义备用UI
    }
    return this.props.children;
  }
}

使用方式:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

3. 如何在React中对props应用验证?

答案:

React中可通过PropTypes库(开发环境)或静态类型检查工具(生产环境)对props进行验证。

  • PropTypes使用(开发环境):
    • 从React 15.5.0版本开始,PropTypes需从prop-types库导入。
    • 定义组件期望的props类型,类型不匹配或缺失必填props时,开发环境控制台会打印警告。

示例代码:

import PropTypes from 'prop-types';

class MyComponent extends React.Component {
  render() { /* 组件渲染逻辑 */ }
}

MyComponent.propTypes = {
  aStringProp: PropTypes.string,
  aNumberProp: PropTypes.number,
  aFunctionProp: PropTypes.func,
  aRequiredProp: PropTypes.number.isRequired,
  anArrayOfNumbers: PropTypes.arrayOf(PropTypes.number),
  anObjectOfShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
};
  • TypeScript类型验证(生产环境): 通过接口(Interfaces)或类型别名(Type Aliases)定义props类型,编译阶段捕获类型错误。

示例代码:

import React from 'react';

interface MyComponentProps {
  aStringProp?: string;
  aNumberProp?: number;
  aFunctionProp?: () => void;
  aRequiredProp: number;
  anArrayOfNumbers?: number[];
  anObjectOfShape?: {
    color?: string;
    fontSize?: number;
  };
}

const MyComponent: React.FC<MyComponentProps> = ({
  aStringProp,
  aNumberProp,
  aFunctionProp,
  aRequiredProp,
  anArrayOfNumbers,
  anObjectOfShape
}) => {
  return <div>{aRequiredProp}</div>;
};

4. 在React中如何创建Refs?创建Refs的方式有什么区别?

答案:

Refs用于获取和操作DOM元素或React组件实例,支持多种创建方式,适用于类组件和函数组件。

  • 类组件中创建Refs:

    1. React.createRef()(推荐):

      • React 16.3版本后推出,API简洁一致,Ref值在组件生命周期中保持不变。
      • 示例代码:
        class MyComponent extends React.Component {
          constructor(props) {
            super(props);
            this.myRef = React.createRef();
          }
          componentDidMount() {
            const node = this.myRef.current; // 访问DOM节点或组件实例
          }
          render() {
            return <div ref={this.myRef} />;
          }
        }
        
    2. 回调Refs:

      • 灵活,可在组件挂载/卸载时执行额外逻辑。
      • 缺点:若回调函数定义在render中,每次渲染会创建新函数实例,可能导致性能问题。
      • 示例代码:
        class MyComponent extends React.Component {
          componentDidMount() {
            // 访问DOM节点或组件实例
          }
          render() {
            return <div ref={(node) => (this.myRef = node)} />;
          }
        }
        
  • 函数组件中创建Refs:

    1. React.useRef()(推荐):

      • Hooks API的一部分,Ref值在组件生命周期中保持不变。
      • 示例代码:
        import React, { useRef, useEffect } from 'react';
        function MyComponent() {
          const myRef = useRef(null);
          useEffect(() => {
            const node = myRef.current; // 访问DOM节点或组件实例
          }, []);
          return <div ref={myRef} />;
        }
        
    2. 回调Refs:

      • 需配合useState存储Ref值,灵活但可能有性能问题。
      • 示例代码:
        import React, { useEffect, useState } from 'react';
        function MyComponent() {
          const [myRef, setMyRef] = useState(null);
          useEffect(() => {
            if (myRef) { // 访问DOM节点或组件实例
            }
          }, [myRef]);
          return <div ref={(node) => setMyRef(node)} />;
        }
        

5. createContext解决了什么问题?React父组件如何与子组件通信?子组件如何改变父组件的状态?

答案:

  • createContext的作用: 解决props"穿透"问题,无需层层传递props,让深层嵌套组件直接访问全局数据(如主题、用户信息)。

示例代码:

import React, { createContext, useContext } from 'react';

// 创建Context对象
const ThemeContext = createContext('light');

function App() {
  // 提供context值
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <Button />;
}

function Button() {
  const theme = useContext(ThemeContext); // 消费context值
  return <button>{theme}</button>;
}
  • 父组件与子组件通信: 父组件通过props向子组件传递数据和函数,子组件通过props获取。

  • 子组件改变父组件状态: 子组件无法直接修改父组件状态,需通过父组件传递的函数间接修改。

示例代码:

import React, { useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <Child onIncrement={handleIncrement} />
    </div>
  );
}

function Child({ onIncrement }) {
  return <button onClick={onIncrement}>Increment</button>;
}

6. memo有什么用途,useMemo和memo区别是什么?useCallback和useMemo有什么区别?

答案:

  • memo的用途: memo是高阶组件,适用于函数组件,类似React.PureComponent,仅在props改变时重新渲染,避免不必要的渲染,优化性能。

示例代码:

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用props渲染 */
});
  • useMemo和memo的区别:

    特性useMemomemo
    类型Hook高阶组件
    作用对象昂贵的计算操作函数组件的渲染
    核心逻辑依赖项改变时重新计算缓存值props改变时重新渲染组件
    示例代码const val = useMemo(() => compute(a,b), [a,b])const Comp = React.memo(MyComp)
  • useCallback和useMemo的区别:

    特性useCallbackuseMemo
    缓存对象回调函数计算结果值
    适用场景避免函数重复创建(如传递给子组件的回调)避免昂贵计算重复执行
    示例代码const fn = useCallback(() => do(a,b), [a,b])const val = useMemo(() => compute(a,b), [a,b])

7. React新老生命周期的区别是什么?合并老生命周期的理由是什么?

答案:

  • 新老生命周期的区别(以React 16.3为分界):

    • 移除的生命周期(不安全):componentWillMount、componentWillReceiveProps、componentWillUpdate(异步渲染中可能多次调用,导致问题)。
    • 新增的生命周期:getDerivedStateFromProps(渲染前更新状态,替代componentWillReceiveProps部分功能)、getSnapshotBeforeUpdate(DOM更新前捕获DOM信息,替代componentWillUpdate部分功能)。
    • 修改的生命周期:componentDidUpdate、componentDidCatch(功能不变,在提交阶段调用,适配异步渲染)。
    • 保留的安全生命周期:componentDidMount、shouldComponentUpdate、componentWillUnmount。
  • 合并老生命周期的理由:

    1. 老生命周期在异步渲染模式下存在潜在问题(如多次调用导致状态不一致)。
    2. 新生命周期更适配异步渲染和性能优化,逻辑更清晰,利于React未来发展。

8. React中的状态管理库你如何选择?什么是状态撕裂?useState同步还是异步?

答案:

  • 状态管理库选择:
    1. Redux:适用于大型应用、跨组件共享复杂状态,集中式状态存储,严格的"单一数据源、不可变状态"原则。
    2. MobX:响应式状态管理,支持多状态源、可变状态,灵活,适合快速原型开发。
    3. Context API + Hooks(useState/useReducer):适用于小型应用或简单组件间状态共享,无需额外依赖。
    4. Jotai:轻量级原子状态管理,状态分解为"原子",细粒度订阅,仅依赖状态的组件重新渲染,适配Concurrent Mode。

Jotai示例代码:

import { atom, useAtom } from 'jotai';

// 创建原子状态
const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setCount(count + 1)}>increment</button>
    </div>
  );
}
  • 状态撕裂: 在并发渲染(Concurrent Mode)中,由于渲染优先级不同,应用不同部分可能看到同一份共享状态不一致的现象。React通过React Server Components、useTransition、useDeferredValue等特性解决。

  • useState的同步/异步特性:

    • React 18之前:同步环境中异步(React上下文内,状态更新合并),异步环境中同步(如setTimeout、Promise.then、ajax回调,脱离React上下文)。
    • React 18之后:无论同步/异步环境,setState均为异步(基于useReducer重写,支持自动批处理)。

9. 在React中什么是Portal?

答案:

Portal提供了将子节点渲染到父组件DOM层次结构之外的DOM节点的方式,解决父组件样式(如overflow:hidden、z-index)对於单子组件布局的影响。

常见应用场景:模态框(Modal)、提示框(Tooltips)等需要"跳出"父容器的组件。

示例代码:

import ReactDOM from 'react-dom';

class Modal extends React.Component {
  el = document.createElement('div');

  componentDidMount() {
    // 将div添加到body
    document.body.appendChild(this.el);
  }

  componentWillUnmount() {
    // 从body移除div
    document.body.removeChild(this.el);
  }

  render() {
    // 渲染子元素到指定DOM节点
    return ReactDOM.createPortal(
      this.props.children,
      this.el
    );
  }
}

10. 自己实现一个Hooks的关键点在哪里?

答案:

自定义Hook是遵循特定规则的JavaScript函数,核心关键点如下:

  • 基础规则:

    1. 函数名称必须以use开头(React依赖该约定检查Hook规则,增强代码可读性)。
    2. 只能在函数组件或其他Hook中调用Hook(保证Hook调用顺序一致,React正确维护内部状态)。
  • 实现示例(计数器Hook):

import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);
  return { count, increment, decrement };
}

// 组件中使用
function Counter() {
  const { count, increment, decrement } = useCounter();
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}
  • TypeScript实现注意事项:
    1. 为Hook参数指定类型(如initialValue: number)。
    2. 为返回值指定类型(如定义UseCounterReturn接口)。
    3. 为内部状态和函数指定类型。

TypeScript示例:

import { useState } from 'react';

type UseCounterReturn = {
  count: number;
  increment: () => void;
  decrement: () => void;
};

function useCounter(initialValue: number = 0): UseCounterReturn {
  const [count, setCount] = useState<number>(initialValue);
  const increment = (): void => setCount(prev => prev + 1);
  const decrement = (): void => setCount(prev => prev - 1);
  return { count, increment, decrement };
}

11. 你去实现React的具体业务的时候TS类型不知道怎么设置你会怎么办?

答案:

  1. 翻阅React.d.ts文档(优先推荐,官方类型定义权威)。
  2. 搜索谷歌(查找社区解决方案、官方示例或技术博客)。

12. React和其他框架对比优缺点是什么?你们团队选择React的理由是什么?

答案:

  • React的优点:

    1. 组件化与Hooks:函数组件+Hooks简化状态和生命周期管理,组件可单独测试和重用。
    2. 性能优化:支持并发更新模式、FID优化,Fiber架构提升渲染效率。
    3. 强大社区支持:丰富的开源库、学习资源和活跃社区。
    4. 灵活的状态管理:内置Context API+Hooks,支持Redux、MobX、Jotai等第三方库。
  • React的缺点:

    1. 仅关注视图层:需额外选择路由(如React Router)、状态管理等库,增加项目复杂度。
    2. 频繁更新:社区活跃导致特性迭代快,需持续学习新概念和最佳实践。
    3. 学习曲线陡峭:高性能React应用需掌握Hook优化、避免不必要渲染等技巧。
  • 团队选择React的常见理由:

    1. 技术栈一致性:已有项目使用React,保持统一提升开发效率。
    2. 团队技能匹配:成员熟悉React,减少新框架学习成本。
    3. 项目需求适配:需构建复杂用户界面,追求高灵活性。

13. React16/17/18都有哪些新变化?useTransition是啥提解决了什么?

答案:

  • React 16 新变化:

    1. 引入Fiber架构(核心算法重构)。
    2. 错误边界(Error Boundaries):捕获子组件错误,避免应用崩溃。
    3. Fragments和Strings:组件可返回多个元素或字符串。
    4. Portals:跨DOM层级渲染子节点。
    5. 改进服务器端渲染:支持流式渲染和组件缓存。
  • React 17 新变化:

    1. 事件委托机制修改:事件处理程序附加到根DOM容器,而非document,适配多版本React共存。
    2. 平滑升级支持:允许同一应用运行多个React版本,简化升级流程。
  • React 18 新变化:

    1. 并发模式(Concurrent Mode):渲染过程中让出控制权给浏览器,优化用户交互响应。
    2. React Server Components:仅在服务器运行,无需发送到客户端,提升渲染性能、减少客户端代码。
    3. 自动批处理(automatic batching):任何场景下合并多个状态更新为一次渲染。
  • useTransition的作用与解决问题: useTransition是React 18的新Hook,用于并发模式下避免UI阻塞,实现平滑的视觉更新。

核心功能:

  • 触发过渡状态更新,标记低优先级更新,不阻塞高优先级操作(如用户输入)。
  • 返回[startTransition, isPending]:startTransition包裹低优先级更新逻辑,isPending标识是否处于过渡中。

解决问题:数据加载等耗时操作时,避免UI冻结,先显示加载状态,数据加载完成后平滑更新UI。

示例代码:

import { useState, useTransition, Suspense } from 'react';

// 模拟异步数据加载
const fetchData = () => {
  return new Promise(resolve => setTimeout(() => resolve('Loaded data'), 2000));
};

const AsyncComponent = () => {
  const [data, setData] = useState(null);
  const [startTransition, isPending] = useTransition();

  const loadData = () => {
    startTransition(() => {
      fetchData().then(result => setData(result));
    });
  };

  return (
    <div>
      <button onClick={loadData}>Load Data</button>
      {isPending ? 'Loading...' : data}
    </div>
  );
};

const App = () => {
  return (
    <Suspense fallback="Loading">
      <AsyncComponent />
    </Suspense>
  );
};

二、源码高频考题

1. React整体渲染流程请描述一下?双缓存是在哪个阶段设置的?优缺点是什么?

答案:

  • React整体渲染流程(三阶段模型):

    1. Scheduler(调度阶段)

      • 接收更新请求(如 setState、props 变化),根据 Lane 优先级模型对任务排序
      • 高优先级任务(用户交互)可打断低优先级任务(数据预加载)
      • 利用 MessageChannel(优先)或 setTimeout 实现时间切片,避免阻塞主线程
    2. Render(渲染/协调阶段) ⚡ 可中断

      • 从根节点开始深度优先遍历,构建 workInProgress Fiber 树
      • 执行组件函数(函数组件)或 render 方法(类组件),按顺序调用 Hooks
      • 通过 Diff 算法比较新旧 Fiber 节点,为变化节点标记副作用(Placement / Update / Deletion)
      • 最终输出 Effect List(副作用链表),供 Commit 阶段消费
      • 注意:此阶段不操作真实 DOM,纯 JavaScript 计算
    3. Commit(提交阶段) 🔒 不可中断

      • Before Mutation 子阶段:读取 DOM 状态(如 getSnapshotBeforeUpdate)
      • Mutation 子阶段:根据 Effect List 执行真实 DOM 操作(增删改)
      • Layout 子阶段:执行 useLayoutEffect 回调、componentDidMount / componentDidUpdate
      • 切换 current 指针:root.current = finishedWork,完成双缓存切换
  • 双缓存的设置阶段与优缺点:

    • 创建时机:Render 阶段开始时,通过 createWorkInProgress 函数基于 current Fiber 树克隆创建 workInProgress Fiber 树
    • 切换时机:Commit 阶段结束时,执行 root.current = finishedWork,workInProgress 树变为新的 current 树
    • 核心逻辑:始终维护两棵 Fiber 树——current(当前屏幕显示)和 workInProgress(内存中构建),交替使用
    • 优点:
      1. 批量更新:DOM 操作集中在 Commit 阶段执行,减少重绘回流
      2. 错误回退:渲染出错时可丢弃 workInProgress 树,回退到 current 树的稳定状态
      3. 支持并发:可随时中断 workInProgress 树构建,优先处理高优先级任务
      4. 无闪烁:新 UI 完全构建完成后才切换显示,避免中间状态
    • 缺点:增加内存占用(同时维护两棵树),实现复杂度提升

2. Fiber架构原理你能细致描述下么?

答案:

Fiber是React 16引入的新调和(reconciliation)引擎,核心目标是支持时间切片、并发模式,实现渲染过程的暂停、中断和恢复。

核心原理与关键概念:

  1. Fiber Node(Fiber节点):

    • 每个React组件对应一个Fiber节点,存储组件状态、类型、DOM信息及链表指针(父、子、兄弟节点),形成Fiber树。
    • 链表结构支持遍历中断与恢复(替代传统栈递归遍历)。
  2. 双缓冲技术:

    • 维护current Fiber树(当前DOM对应的树)和work-in-progress Fiber树(正在构建的新树),构建完成后切换根节点指针完成DOM更新。
  3. 工作循环:

    • 遍历Fiber树,为每个节点执行"工作单元"(调用生命周期、生成DOM更新)。
    • 遍历过程中可响应高优先级任务,暂停当前工作,处理完毕后恢复。
  4. 时间切片(React 18前):

    • 将渲染工作分解为多个小任务,每个任务执行时间不超过阈值(如16ms),避免阻塞浏览器主线程(用户输入、动画)。
    • React 18后改为"微任务+宏任务"实现,替代时间切片。
  5. 优先级与并发:

    • 不同更新类型分配不同优先级(如用户交互>数据加载),高优先级任务可打断低优先级任务。
    • 支持并发渲染,多个更新任务并行处理(逻辑上)。
  6. 错误边界集成:

    • 组件渲染出错时,沿Fiber树向上查找最近的错误边界组件,传递错误并显示备用UI。

3. React Scheduler核心原理?React 16/17/18变化都有哪些?Batching在这个阶段里么,解决了什么?原理是什么?

答案:

  • React Scheduler核心原理: Scheduler是React内部任务调度库,核心作用是切分长期渲染任务,利用浏览器空闲时间执行,优先响应高优先级任务(如用户输入、动画),实现时间切片和应用响应性优化。

核心机制:

  • 使用requestIdleCallback(降级方案:setTimeout)调度任务,在浏览器空闲时段执行任务。

  • 任务优先级排序:高优先级任务(如click事件)打断低优先级任务(如列表渲染)。

  • React 16/17/18中Scheduler的变化:

    1. React 16:实验性引入,配合Fiber架构实现时间切片,仅支持基础任务调度。
    2. React 17:无明显变化,重点优化事件系统,Scheduler保持兼容。
    3. React 18:深度整合,支持并发模式和自动批处理,调度策略适配新特性(如Suspense、useTransition)。
  • Batching(批处理)相关:

    • 是否在Scheduler阶段:是,Batching是Scheduler调度过程中的核心优化机制。
    • 解决的问题:合并多个状态更新为一次渲染,减少DOM操作和组件重渲染次数,提升性能。
    • 原理:
      1. 传统Batching(React 18前):仅在React事件处理函数、生命周期方法中生效,异步环境(setTimeout、Promise)中不生效(每次更新触发一次渲染)。
      2. 自动批处理(React 18后):任何场景下(同步/异步)的连续状态更新均会被合并,无需手动处理。

示例代码(React 18自动批处理):

// React 18中仅触发一次渲染
setTimeout(() => {
  setCount(count + 1);
  setCount(count + 1);
}, 1000);

4. Hooks为什么不能写在条件判断、函数体里?我现在有业务场景就需要在if里写怎么办呢?

答案:

  • Hooks不能写在条件判断、函数体里的原因: React Hooks通过"单向链表"跟踪组件的Hook状态和副作用,依赖Hook调用顺序的一致性:
    1. 组件每次渲染时,React按链表顺序执行Hook,通过索引匹配状态(如第一个useState对应链表第一个节点)。
    2. 若在条件判断(if)、循环或函数体内部写Hook,会导致每次渲染时Hook调用顺序改变,链表索引错乱,状态跟踪失败(如状态丢失、类型错误)。

错误示例:

// 错误:Hook写在if条件中
function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0); // 可能导致调用顺序不一致
  }
  return <div>{count}</div>;
}
  • 业务场景需要在条件中使用的解决方案:
    1. 提前声明Hook,在条件中使用其返回值(推荐):

      function MyComponent() {
        const [count, setCount] = useState(0); // 顶部声明,保证顺序一致
        if (someCondition) {
          setCount(10); // 条件中使用Hook返回的函数
        }
        return <div>{count}</div>;
      }
      
    2. 使用组件外状态管理库:如Zustand、Jotai等,状态存储在组件外部,不受Hook调用顺序限制。

    3. 拆分组件:将条件逻辑封装为独立组件,在父组件中通过条件渲染子组件,Hook在子组件内部正常声明。

      function ConditionalComponent() {
        const [count, setCount] = useState(0); // 子组件内部声明Hook
        return <div>{count}</div>;
      }
      
      function MyComponent() {
        if (someCondition) {
          return <ConditionalComponent />; // 条件渲染组件
        }
        return <div>Default</div>;
      }
      

5. useState 直接在函数组件调用会造成无限渲染,原因是什么?怎么监控 React 无意义渲染,监控的原理是什么?

答案

  • 无限渲染原因:当在函数组件主体部分直接调用 setState 时,每次组件渲染都会执行 setState,而 setState 又会触发组件重新渲染,形成无限循环。
    • 错误示例:
    function MyComponent(){
      const [count, setCount] = React.useState(0);
      // 直接调用 setCount 会导致无限循环
      setCount(count + 1);
      return <div>{count}</div>;
    }
    
    • 正确示例(在 useEffect 中调用,仅组件挂载时执行):
    function MyComponent(){
      const [count, setCount] = React.useState(0);
      React.useEffect(() => {
        setCount(count + 1);
      }, []); // 空数组表示无依赖,仅挂载和卸载时执行
      return <div>{count}</div>;
    }
    
  • 无意义渲染监控工具:@welldone-software/why-did-you-render 库。
  • 监控原理:该库通过包装 React 组件,重写类组件的 shouldComponentUpdate 方法或函数组件的 React.memo 方法,在这些方法中对比前后两次渲染的 props 和 state 值。若 props/state 无真正变化但组件仍重渲染,会在控制台打印警告,包含组件名称、前后值及解决建议。
  • 注意:该库仅提供问题定位信息,需通过合理使用 useState、useEffect、useMemo、useCallback、shouldComponentUpdate 或 React.memo 优化渲染。

6. Dom Diff 细节请详细描述一下?Vue 使用了双指针,React 为什么没采用呢?

答案

  • React DOM Diff 核心逻辑:React 的更新过程包含新旧虚拟 DOM 树对比和 DOM 更新两步,核心是"同层对比"+"特定数据结构优化"。
    • React 16 之前:对比与更新同步进行,采用递归深度遍历,将节点及子节点压入栈中依次访问,回溯完成整棵树 Diff。缺点是递归不可中断,树层级较深时,JS 执行时间过长,阻塞页面渲染和用户交互,导致卡顿。
    • React 16 及之后:
      1. 将递归改为迭代,仅对比同层节点(不同层级节点直接销毁重建,不跨层 Diff);
      2. 栈结构改进为 Fiber 链表结构,实现更新过程可随时中断(配合时间切片)。
  • React 未采用双指针的原因:React 的 Diff 设计核心是"牺牲部分最优解换取更稳定的性能和更灵活的调度能力"。双指针算法更适用于有序列表的精准位置对比,但 React 需处理更复杂的 DOM 结构(如跨层级节点、非列表节点),且后续引入的 Concurrent Mode、Fiber 架构要求 Diff 过程可中断、可恢复。同层对比 + Fiber 链表的设计,能更好地适配这些调度需求,同时简化复杂 DOM 结构的 Diff 逻辑,平衡性能与灵活性。

7. React 如何实现自身的事件系统?什么叫合成事件?

答案

  • 合成事件(SyntheticEvent)定义:React 自行实现的一套事件系统,模拟原生 DOM 事件,提供更优特性,且使用方式(如 onClick、onChange)与原生 DOM 事件类似。
  • 合成事件核心特性:
    1. 跨浏览器兼容性:统一不同浏览器的原生事件行为差异,提供一致 API。
    2. 性能优化:采用事件委托机制,不将事件处理器直接绑定到 DOM 节点,而是在根节点(React 16 及之前为 document,React 17 及之后为渲染 React 树的根 DOM 容器)绑定统一监听器,通过内部映射找到真正的事件处理器,减少监听器数量,节省内存。
    3. 集成状态系统:与组件生命周期、状态系统紧密结合,事件处理函数中可直接调用 setState,React 会自动批处理更新并重新渲染。
    4. 提供丰富信息:包含 event.target 等比原生事件更全面的信息。
  • React 17 事件系统变更:不再向 document 附加事件处理器,而是绑定到每个 React 树的根 DOM 容器(如 $("#app")、$("#header")),解决多 React 版本嵌套时 e.stopPropagation 失效的问题。

8. React Concurrent Mode 是什么?React18 是怎么实现的?他和 useTransition 有联系么?

答案

  • Concurrent Mode 定义:React 的一种新渲染模式,核心是"时间切片",使 React 能在多个状态更新中拆分渲染任务为多个短任务,任务间预留空闲时间供浏览器处理用户输入、动画等其他任务,避免长时间渲染阻塞主线程,提升应用响应性(尤其复杂 UI 或低性能设备)。
  • 传统同步渲染与 Concurrent Mode 对比:
    • 传统模式:状态更新时阻塞主线程,直至所有组件渲染完成,可能导致应用无响应;
    • Concurrent Mode:拆分渲染任务,可中断、恢复,不阻塞主线程。
  • React 18 实现方式:React 18 正式支持 Concurrent Mode,通过重构渲染调度机制,结合 Fiber 架构的可中断特性,实现任务的拆分与优先级调度。
  • 与 useTransition 的联系:
    • useTransition 是 React 18 引入的新 Hook,与 Concurrent Mode 紧密相关;
    • 作用:标记状态更新为"过渡更新",告知 React 该更新可延迟,优先保证页面响应性。例如数据加载类更新,React 会先显示旧 UI,待数据准备完成后平滑过渡到新状态,避免界面抖动;
    • 核心关联:Concurrent Mode 提供"可中断渲染"的底层能力,useTransition 是基于该能力的上层 API,让开发者可主动标记低优先级更新,优化用户体验。

9. 将 Vue 换成 React 能提高 FPS 么?请给出理由

答案

  • 结论:不一定,FPS(帧率)提升与否取决于多种因素,框架本身不是核心决定因素。
  • 核心影响因素与框架差异:
    1. 虚拟 DOM 实现:
      • Vue:可跟踪依赖关系,精准更新变化部分,无需重渲染整个组件树;
      • React:需通过 shouldComponentUpdate、React.memo 等手动优化避免无意义渲染。
    2. 异步渲染:
      • React:支持 Concurrent Mode 和 useTransition,处理大量更新时保持界面响应,利于复杂交互/动画场景的 FPS 提升;
      • Vue:暂无类似特性,但正在推进相关优化。
    3. 框架大小:Vue 通常略小于 React,可能加快首次加载和解析速度,但对 FPS 影响较小。
  • 建议:框架迁移成本高且结果不确定,优先优化现有代码(如 Vue 用异步组件、优化依赖追踪;React 用内置优化 Hook),仅当确认框架特性与应用场景高度匹配时,再考虑迁移(需保证测试环境一致且对目标框架足够了解)。

10. Lane 是什么?解决了 React 什么问题?原理是什么?

答案

  • Lane 定义:React 17 引入的"车道模型",是用于任务优先级调度的核心机制,与 Concurrent Mode 配合支持 React Suspense。
  • 解决的问题:React 16 中,即使启用 Concurrent Mode,多个 Suspense 组件同时加载数据时,可能阻塞其他更新甚至整个应用,需等待所有数据加载完成才继续渲染,导致渲染延迟和用户体验下降。
  • 核心原理:
    • Lane 为不同更新任务分配"优先级车道",Suspense 组件的异步更新可被分配到特定 Lane;
    • React 可根据 Lane 优先级调度任务,暂时推迟低优先级的 Suspense 异步更新,优先处理高优先级更新(如用户输入),从而优化应用响应速度和性能,更好地处理复杂异步更新场景。

三、同构与服务端渲染

1. React 同构开发你是如何部署的?使用 Next 还是自己开发的?流式渲染是什么?有什么好处?

答案

  • 同构开发定义:又称 Isomorphic JavaScript,同一份 React 代码可在服务器端执行生成 HTML,浏览器端接管渲染并支持用户交互。
  • 同构开发优势:
    1. 提升首屏渲染速度;
    2. 利于 SEO(搜索引擎可直接解析服务器返回的 HTML)。
  • 部署方式:
    1. 使用 Next.js:基于 React 的通用框架,内置路由、数据预取、预渲染等能力,节省开发时间,提供成熟稳定的部署方案;
    2. 自行开发:需用 Node.js 服务器(如 Express)预渲染 React 应用,手动解决代码分割、数据预取、路由匹配等问题;
    3. 云平台选择:常用 AWS、Cloudflare。
  • 部署注意事项:服务器需部署 Node.js 环境,配置 Nginx 等反向代理转发请求到 Node.js 服务器。
  • 流式渲染(Stream Rendering):
    • 定义:服务器生成 HTML 时,以数据流形式逐步发送到客户端,而非等待全部 HTML 生成后一次性发送。
    • 好处:
      1. 提升首屏渲染速度(浏览器可提前解析渲染部分页面);
      2. 减少服务器内存占用(无需保存完整 HTML 字符串)。

2. React 服务端渲染需要进行 Hydrate 么?哪些版本需要?据我所了解 Qwik 是去调和的,为什么呢?

答案

  • React 服务端渲染(SSR)与 Hydrate 的关系:
    • Hydrate 是 SSR 的关键步骤:服务器端生成 HTML 后,浏览器端需通过 Hydrate 接管 HTML,使其变得可交互(绑定事件、恢复状态)。
    • 版本要求:
      • React 16 及之后:需使用 ReactDOM.hydrate 替代 ReactDOM.render 进行 Hydrate;
      • React 18 及之后:将引入"部分 Hydrate"能力,初始渲染后不立即 Hydrate 全部组件,按需(根据用户交互或组件优先级)进行,仍需 Hydrate 保证交互性。
  • Qwik 去调和(无 Diffing)的原因:
    • Qwik 是 Google 推出的性能优化型框架,核心理念是"HTML-oriented programming"(HTML 导向编程),认为 HTML 本身应描述应用状态及状态转换逻辑。
    • 实现方式:组件直接链接到 HTML 元素,事件处理器直接绑定到用户操作,无 JavaScript 时浏览器仍可正常渲染和导航。
    • 无需 Hydrate/Diff 的核心:Qwik 直接复用服务器渲染的 HTML,无需通过 JavaScript 创建/更新 DOM,也无需对比虚拟 DOM(Diff),减少首次内容绘制(FCP)时间,提升加载速度。
    • 局限性:不适用于复杂单页应用(SPA)或需频繁更新状态的场景。

3. React 同构渲染如何提高性能?你是怎么落地的?同构解决了哪些性能指标?

答案

  • 同构渲染的性能优势:
    1. 提升首屏加载速度:服务器预渲染 HTML,浏览器无需等待全部 JavaScript 下载解析即可显示页面;
    2. 利于 SEO:搜索引擎易解析服务器渲染的 HTML。
  • 优化落地方式:
    1. 数据预取:组件中定义静态 loadData 方法,或使用 Next.js 自动处理数据预取;
    2. 兼容生命周期:避免依赖仅客户端执行的生命周期方法(如 componentDidMount、componentDidUpdate);
    3. 服务器性能优化:采用流式渲染、合理设置缓存策略,减少服务器 CPU/内存占用;
    4. 框架选择:优先使用 Next.js,简化路由、构建、优化等工作。
  • 影响的性能指标:
    • 核心指标(Google Web Vitals):
      1. LCP(Largest Contentful Paint,最大内容绘制):减少页面主要内容的呈现时间;
      2. FID(First Input Delay,首次输入延迟):提升页面交互响应速度;
      3. CLS(Cumulative Layout Shift,累积布局偏移):减少页面加载过程中的视觉偏移,提升稳定性。
    • 历史指标(已淘汰):FMP(First Meaningful Paint,首次有意义绘制),同构渲染曾可优化该指标。

4. 同构注水脱水是什么意思?React 进行 ServerLess 渲染时候项目需要做哪些改变?

答案

  • 注水(Rehydration)与脱水(Dehydration):
    1. 脱水(Dehydration):服务器端渲染阶段,React 将组件树渲染为 HTML 字符串,并生成应用状态相关的"脱水数据",随 HTML 一起发送到客户端;
    2. 注水(Rehydration):客户端接收 HTML 和脱水数据后,React 用脱水数据恢复应用状态,匹配服务器渲染的 HTML 与客户端组件树,接管 DOM 使其可交互。
  • 同构渲染的优缺点:
    • 优点:首屏显示快、客户端无需重复获取初始数据、复用 DOM 减少额外操作;
    • 缺点:增加服务器负载、可能出现数据不一致或状态错误。
  • React Serverless 渲染的项目改造:
    1. 数据预取适配:确保数据预取逻辑兼容 Serverless 环境(如无持久化服务器状态);
    2. 依赖精简:减少不必要的依赖,降低冷启动时间;
    3. 缓存策略:合理设置数据缓存(如 CDN 缓存静态资源、Serverless 函数结果缓存);
    4. 框架支持:优先使用 Next.js 等支持 Serverless 部署的框架,简化配置(如 Vercel、Cloudflare Pages 部署);
    5. 环境适配:处理 Serverless 环境的临时文件、环境变量等限制。

5. 不同平台进行 JS 冷启动的区别是什么?

答案

  • 核心对比对象:Cloudflare Workers 与 AWS Lambda(主流 Serverless 平台)。
  • 冷启动差异本质:运行环境与实例启动机制不同。
    1. Cloudflare Workers:
      • 运行环境:基于 V8 Isolate(Google V8 引擎的轻量级实例);
      • 冷启动特点:Isolate 轻量,创建/销毁速度极快,冷启动时间接近零;
      • 核心逻辑:每个请求对应新 Isolate,拥有独立全局变量和执行栈,无容器启动开销。
    2. AWS Lambda:
      • 运行环境:Node.js 运行环境,函数打包为容器启动;
      • 冷启动特点:容器启动重量级,冷启动时间较长(需分配计算资源、加载代码及依赖、执行初始化逻辑);
      • 优化机制:频繁调用的函数会保持"热状态",后续请求可快速响应。
  • 总结:Cloudflare Workers 冷启动速度远快于 AWS Lambda,适用于对响应速度敏感的场景;AWS Lambda 热状态下性能稳定,适用于依赖复杂、调用频率中等的场景。

6. 源码 scheduler 下 fork 的 Scheduler.js-209 行实现了什么?参数有几个?返回有几个?

答案

文档中未提供该问题的具体答案,需查阅 React 源码中 scheduler 模块的 Scheduler.js 第 209 行具体实现逻辑。

最后更新时间: 2025/11/24 22:42
Contributors: lingtong, Meadery