Skip to content
On this page

React 面试题合集

什么是 React

React 是一个 JS UI 库,用于构建高效、快速的用户的界面。特点:

  • 遵循组件式开发声明式编程范式函数式编程思想

    声明式编程是一种编程范式,它描述目标的性质,让计算机明白目标,而不是流程。命令式编程与之对立,它需要明确告诉计算机每一步怎么做。

  • 使用虚拟 DOM 高效的操作 DOM
  • 遵循单向数据流

key 的作用是什么

key 是虚拟 DOM 的唯一标识,用于判断元素是 新创建的 还是 被移动的 元素,从而减少不必要的 Diff。

什么是虚拟 DOM、更新原理

  • 为什么需要虚拟 DOM

    • 在前端性能优化中,有一个很重要的方式就是尽可能少的操作 DOM,不仅仅是 DOM 操作相对较慢,更是因为频繁的 DOM 操作会造成浏览器的回流或者重绘,这些都会对我们的性能造成影响
    • 降低 XSS 风险
    • 跨平台的成本更低
  • 虚拟 DOM 是什么

虚拟 DOM 是页面中真实 DOM 元素的 JS 表示形式。Diff 比较之后生成的结果称为 patch,之后根据 patch 进行真实 DOM 操作。

  • 虚拟 DOM 更新原理

每当有更新发生时,Reconciler(协调器)会做如下工作:

  1. 调用函数组件、或 class 组件的 render 方法,将返回的 JSX 转化为虚拟 DOM(整个 DOM 副本保存为虚拟 DOM)
  2. 将虚拟 DOM 和上次更新时的虚拟 DOM 对比
  3. 通过对比找出本次更新中变化的虚拟 DOM
  4. 通知 Renderer(渲染器)将变化的虚拟 DOM 渲染到页面上

虚拟 DOM 是怎么对比的、Diff 算法

基本流程:

  1. 真实 DOM 映射为虚拟 DOM
  2. 虚拟 DOM 变化后,Diff 算法生成 patchpatch 是结构化的数据,包括增加、更新、移除等)
  3. 根据 patch 更新真实 DOM

Diff 注意点:

  • 更新时机:更新发生在 setState 节点的状态发生变化之后。
  • 遍历算法:深度优先遍历。
  • 优化策略:为了降低时间复杂度,React 采用“分治思想”,如下:
    🔴 策略一: 忽略节点跨层级操作场景,提升比对效率。 进行树比对时,两棵树只对同一层次的节点进行比较,如果发现节点不存在,则该节点及其子节点会被完全删除。
    🟡 策略二: 如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。 如果组件是同一类型,则进行树比对,如果不是则直接放入 patch
    🟢 策略三: 同一层级子节点,可通过标记 key 的方式进行列表对比。 通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。

fiber 对 diff 算法的影响:

  • fiber 机制下,节点与树分别采用 FiberNode 和 FiberTree 进行重构
  • fiber 机制下整个更新过程由 current 与 workInProgress 两株树双缓冲构成

什么是 jsx、渲染原理

JSX 是一个语法拓展,结合了 JS 和 HTML,结构清晰明了。
JSX 是一个语法糖,在编译时会被 Babel 编译为 React.createElement 方法。

setState 是同步还是异步

setState 并不是真正的异步函数,它实际是通过队列延迟操作来模拟实现的。
通过 isBatchingUpdates 来判断 setState 是先存进队列,还是直接更新。值为 true 则执行异步操作,false 则同步更新。

  • 在 React 的生命周期事件合成事件中可拿到 isBatchingUpdates 控制权,将状态放进队列,控制执行节奏。
  • 而在 addEventListener setTimeout 这些原生事件中,isBatchingUpdates 为 false,setState 会同步更新。

合成事件:原理类似于“事件委托”。
1. React 给 <div id="root"> 挂上监听事件
2. DOM 触发事件后冒泡到 root
3. React 找到对应的 DOM,生成一个合成事件
4. 按组件树模拟一遍事件冒泡

为什么 props 是只读的

  • 单向数据流

    如果 props 可以在子组件修改,那么 props 会变得难以预测

  • 纯函数,函数式编程

    1. 给定相同的输入,总是得到相同的输出
    2. 过程中没有副作用(副作用:外部可观察的变化)

纯函数的好处
1. 更容易进行测试,结果只依赖输入
2. 更容易调用,不用担心副作用
3. 结果可以缓存,因为结果只依赖输入

react 和 vue 的区别

相同点:

  • 都是创建 UI 界面的 JS 框架
  • 都是组件化开发思想
  • 都使用了虚拟 DOM,来提升渲染速度

不同点:

  1. 数据流
    a. vue 的思想是响应式(MVVM),实现了数据的双向绑定
    b. react 是函数式思想,单向数据流

  2. 模板语法
    a. vue 使用 template 模板,页面是通过 vue 指令+模板语法 实现
    b. react 使用 jsx 模板,通过原生 JS 实现,比如插值,条件,循环,map,if,a&&b

  3. 渲染机制
    a. vue 跟踪组件的依赖关系,vue 是数据变化通知依赖项精确的驱动渲染,不需要渲染整个组件树
    b. react 在应用的状态改变时,重新渲染整个组件树

  4. Diff 算法
    a. vue Diff 使用双向链表,边对比边更新
    b. react Diff 将需要更新的部分添加到消息队列,批量更新

  5. 事件机制
    a. vue 是原生事件
    b. react 是合成事件

React 单向数据流是什么

单向数据流是指数据的流向只能由父组件通过 props 将数据传递给子组件,不能由子组件向父组件传递数据。

要想实现数据的双向绑定,只能由子组件接收父组件 props 传过来的方法去改变父组件的数据,而不是直接将子组件的数据传递给父组件。

React 组件间通信的方法

  • 父组件向子组件通信 => 父级通过 props 向子组件传递需要的信息
  • 子组件向父组件通信 => 通过 props+回调函数 的方式
  • 祖孙(跨级)组件通信 => createContext + useContext
  • 兄弟组件通信 => 找到共同的父节点
  • 复杂的组件间通信 => 全局状态管理库,redux zustand 等

Hooks 是什么,为什么要用 hooks

Hooks 是 React 16.8 的新增特性,它可以让你在不编写 class 组件的情况下,使用 state 以及其他的 React 特性。

👉 解决了哪些问题:

  • 在组件之间复用状态逻辑很难

❌ 之前的解决方案,比如 render-props高阶组件,这类方案需要重新组织你的组件结构,它们和其他抽象层组成的组件会形成“嵌套地狱”,这可能会很麻烦,使你的代码难以理解。
✔️ 你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。这使得在组件间或社区内共享 Hook 变得更便捷。

  • 复杂组件变得难以理解:生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。

❌ 我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。如此很容易产生 bug,并且导致逻辑不一致。
✔️ 为了解决这个问题,Hook 允许将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。

  • 难以理解的 class。

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。

✨ 总的来说:

类式组件在多年的应用实践中,发现了很多无法避免问题而又难以解决 😩,而相对类组件,函数组件又太过于简陋 🙃。比如:

  • 类组件可以访问生命周期方法,函数组件不能
  • 类组件中可以定义并维护 state(状态),而函数组件不可以
  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以

但是,函数式编程方式在 JS 中确实比 Class 的面向对象方式更加友好直观,那么只要能够将函数的组件能力补齐,也就解决了上面的问题。
而如果直接修改函数组件的能力,势必会造成更大的成本,最好的方式就是开放对应接口进行调用,非侵入式引入类式组件能力,也就是我们现在看到的 Hooks 了。

Hooks 的使用限制、为什么有这些限制

  1. 只能在 React 函数式组件中调用 Hook
  2. 不可以在循环、条件或者嵌套函数中调用 Hook

Why?

  1. 如上题所说,Hooks 是基于函数式组件设计的,为了引入类式组件能力,所以只支持函数式组件
  2. React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,导致错误。

useEffect 与 useLayoutEffect 的区别

useCallback 与 useMemo 的区别

React 性能优化的手段,避免不必要的 render

  • 可以使用 shouldComponentUpdatePureComponent 来减少因父组件更新而触发子组件的更新,Purecomponent 只做浅层的比较
  • 使用 React.memo 缓存组件的渲染,避免不必要的更新
  • 使用 useMemo 或者 useCallback 缓存变量或者函数
  • 使用 Suspense(react16.6 新增组件)或者 lazy 进行组件的懒加载,Suspense 可以在组件请求数据时展示一个 pending 状态,请求成功后渲染数据
js
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      // 组件加载阶段,显示一个loading...
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}
  • 在显示列表或表格时始终使用 key,这会让 React 的 Diff 速度更快

React 路由的实现原理

基于 hash 的路由:

改变 hash 可以直接通过 location.hash 获取 => 通过监听 hashchange 事件,感知 hash 的变化。

基于 H5 history 路由:

  1. 可以通过 history.pushStateresplaceState 等方法改变 URL,会将 URL 压入栈,同时能够应用 history.go() 等 API
  2. 监听 URL 的变化可以通过自定义事件触发实现