Skip to content
On This Page

面试题[React]

1. React 基础

1.1 React 核心思想

React 的核心思想是:声明式编程组件化单向数据流,配合虚拟 DOM 实现高效更新,并通过 Hooks / 生命周期 管理副作用。

  1. 声明式编程

    • React 采用声明式的编程范式,开发者只需描述 UI “应该是什么样子”,而不必关心如何一步步操作 DOM 来实现。
    • React 会自动处理 DOM 的更新和渲染,让代码更可预测、易于调试。
  2. 组件化

    • React 将 UI 拆分为独立、可复用的组件。每个组件封装自己的结构(JSX)、样式和行为(逻辑),实现关注点分离。
    • 组件可以嵌套组合,形成复杂的应用。
    • 组件分为函数组件(推荐,配合 Hooks)和类组件(早期写法)。
  3. 单向数据流

    • React 采用自上而下的单向数据流:数据从父组件通过 props 传递给子组件,子组件不能直接修改父组件的数据。
    • 数据变化只能通过状态(state)和事件回调向上传递,再由父组件更新状态并重新渲染。
    • 让数据流动清晰可追溯,便于调试和协作。
  4. 虚拟 DOM 与高效更新

    React 在内存中维护一棵轻量的虚拟 DOM 树,当状态变化时:

    1. 重新生成新的虚拟 DOM 树。
    2. 通过 Diff 算法 比较新旧两棵树的差异。
    3. 将最小化的变更批量更新到真实 DOM 上。
    4. 避免了频繁操作真实 DOM 带来的性能开销,提升了渲染效率。
  5. 生命周期 / Hooks 机制

    • 类组件提供生命周期方法(componentDidMountcomponentDidUpdatecomponentWillUnmount)供开发者插入逻辑。
    • 函数组件使用 HooksuseEffectuseCallbackuseMemo 等)来“钩入”状态和生命周期特性,使函数组件也能拥有完整的 React 能力。
  6. 纯函数与副作用分离

    • React 推崇纯函数的渲染逻辑:给定相同的 propsstate,组件应渲染相同的结果。
    • 副作用(如数据请求、DOM 操作、定时器)应放在 useEffect(函数组件)或生命周期方法(类组件)中执行,避免干扰渲染流程。

1.2 类组件和函数式组件的区别

类组件(Class Component)和函数式组件(Functional Component)是 React 中定义组件的两种主要方式。

对比维度类组件函数组件
定义方式继承 React.ComponentReact.PureComponent,必须实现 render() 方法普通 JavaScript 函数,直接返回 JSX
状态管理使用 this.state 读状态,this.setState() 更新状态(合并式更新)使用 useState Hook,返回 [state, setState]setState 替换式更新
生命周期有显式生命周期方法:componentDidMountcomponentDidUpdatecomponentWillUnmount使用 useEffect Hook 组合模拟生命周期,依赖数组控制执行时机
this 问题事件处理函数需要手动绑定 this(或使用箭头函数类属性)没有 this,完全避免绑定困扰
性能优化继承 PureComponent 实现浅比较,或手动 shouldComponentUpdate使用 React.memo 包裹组件,配合 useMemouseCallback 缓存数据和函数
代码复用使用高阶组件(HOC)、render props 模式,容易导致“嵌套地狱”使用自定义 Hook 提取逻辑,更简洁直观
副作用的处理分散在多个生命周期方法中,逻辑需要拆分和重复useEffect 集中管理副作用,按关注点分离(可写多个 Effect)
开发体验代码冗长,需要写很多 this、bind、生命周期;this.setState 合并行为偶尔令人困惑更简洁、函数式,逻辑聚集,借助 Hooks 组合性更强
学习成本需要理解 this、原型链、生命周期顺序只需要理解闭包、Hooks 规则(如不使用条件调用)
未来趋势React 官方不再主动推荐,保持兼容但新特性优先在 Hooks 中实现当前 React 的主流范式,几乎所有新项目、新教程都采用

1.3 JSX

JSX 是 React 中用来描述用户界面的 语法扩展,全称 JavaScript XML。它允许你在 JavaScript 代码中直接编写类似 HTML 的标记,最终会被编译为普通的 JavaScript 函数调用。

本质:JSX → 编译(如 Babel)→ React.createElement() 调用 → React 元素(虚拟 DOM 节点)。

  1. 为什么需要 JSX

    • 声明式构建 UI:用类似 HTML 的结构描述“界面长什么样”,比手动 createElement 更直观。
    • 开发体验好:编辑器支持语法高亮、自动补全;类型检查、语法错误提示更友好。
    • 逻辑与标记融合:JSX 中可以直接嵌入 JavaScript 表达式(用 {}),实现动态渲染。
  2. 基本语法示例

    jsx
    // JSX代码如下
    <div className="sidebar" />
    javascript
    // 转换为以下JS代码
    React.createElement(
      'div',
      { className: 'sidebar' }
    )
  3. JSX 的限制与注意事项

    • 必须只有一个根元素(或使用 <Fragment> / <>...</> 包裹)
    • 元素必须正确闭合(包括自闭合)
    • 属性值除了字符串、{} 表达式外,不能直接写对象或函数(除非作为内联样式或事件处理函数引用)
    • 防止 XSS:React 默认会转义所有嵌入的值,避免注入攻击。

1.4 React Hooks

React Hooks 是 React 16.8 引入的一组函数,它让函数组件能够拥有状态、生命周期等特性,从而彻底改变了组件的编写方式。

简单说:没有 Hooks 时,函数组件只能接收 props 并返回 UI(无状态组件);有了 Hooks,函数组件可以做之前只有类组件才能做的事。

  1. 基本概念

    Hooks 就是钩子,作用是把某个目标结果“钩”到某个可能会变化的数据源或者事件源上。那么当被“钩”到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

  2. React Hooks 带来的优势

    • 状态逻辑复用:通过自定义 Hooks,可以轻松地在多个组件之间复用状态逻辑。
    • 代码简洁性:Hooks 使得函数组件变得更加简洁和易于理解,避免了类组件中的复杂生命周期方法和状态管理。
    • 函数式编程:Hooks 拥抱了函数式编程,使得 React 组件的开发更加符合现代 JavaScript 的开发趋势。
  3. 注意事项

    • 避免过度使用Hooks:虽然Hooks提供了强大的功能,但过度使用可能会导致代码难以理解和维护。因此,在合适的时候使用Hooks是很重要的。
    • 注意Hooks的调用顺序:在函数组件中,Hooks的调用顺序必须是固定的。在组件的不同渲染中,Hooks的调用顺序应该保持一致。
    • 不要在循环、条件或嵌套函数中调用Hooks:这是React的一个规则,必须遵守以避免潜在的错误。
  4. 解决了哪些问题

    • 数据抽象生命周期
    • 组件间状态逻辑复用困难。
    • 复杂组件难以理解。
    • class 组件的学习和维护成本。
    • 提供了更简洁和易于理解的代码。
    • 更灵活的组件设计。
    • 更易于测试。

1.5 React Suspence

会将 html 的读取过程变成 chunk 模式。(分块传输编码)

Suspense 是 React 提供的一个组件,用于处理异步操作,比如数据加载、代码分割等等。最初引入的时候,React 团队的目标是优化用户体验,尤其是在处理异步资源时。通过 Suspense,你可以让 React 在获取必要数据或代码之前,显示一个备用内容或加载状态,以提升用户体验。

具体而言,Suspense 组件解决了以下几个问题:

  • 异步资源加载:当某个组件需要异步获取数据或模块时,Suspense 可以优雅地显示加载状态。
  • 用户体验:通过在数据或代码加载完之前提供一个占位符,Suspense 改善了在网络状况较差或数据获取较慢时的用户体验。
  • 代码分割:配合 React.lazy 使用,Suspense 可以更方便地进行代码分割,减少初始加载时间。

1.6 React Context

React Context是React中一个重要的特性,它提供了一种在组件树中共享数据的方法,而无需通过每个层级显式地传递props。以下是对React Context的详细解释:

  1. 基本概念

    • 定义:Context是React的一个API,它允许你在组件树中传递数据,而不必在每一层组件上显式地通过props传递
    • 作用:Context的主要作用是解决在React应用中,当某些数据需要在组件树中的多个层级之间共享时,通过props逐层传递的繁琐问题。
  2. 创建和使用

    1. 创建Context:使用React.createContext()方法创建一个Context对象。这个对象包含两个重要的组件:ProviderConsumer
      • Provider:用于提供Context的值。它接收一个value属性,这个属性的值会被传递给所有订阅了这个Context的组件。
      • Consumer:用于消费Context的值。它是一个函数组件,接收一个函数作为子元素,这个函数会接收当前的Context值作为参数。
    2. 使用Context
      • 在类组件中使用:可以通过设置static contextType属性来指定要消费的Context,然后在组件内部通过this.context来访问Context的值。
      • 在函数组件中使用:可以使用useContextHook来访问Context的值。useContext接收一个Context对象作为参数,并返回当前Context的值。
  3. 使用场景

    • 跨级传递数据:当需要在组件树中跨越多层组件传递数据时,可以使用Context来避免逐层传递props的繁琐。
    • 全局状态管理:Context可以用于管理全局状态,如当前认证的用户、主题或首选语言等。这些状态通常需要在多个组件之间共享。
    • 避免不必要的组件层次:通过Context,可以避免将数据传递到不需要它的组件中,从而减少组件的层次和复杂度。
  4. 注意事项

    • 默认值:当组件所处的树中没有匹配到Provider时,Context的defaultValue参数会生效。这有助于在不使用Provider包装组件的情况下对组件进行测试。但请注意,将undefined传递给Provider的value时,消费组件的defaultValue不会生效。
    • 嵌套使用:多个Provider可以嵌套使用,里层的Provider会覆盖外层的数据。
    • 性能优化:虽然Context提供了一种方便的数据传递方式,但过度使用可能会导致性能问题。因此,在使用Context时,应尽量避免不必要的订阅和更新。
  5. 示例代码

    jsx
    import React, { createContext, useContext, useState } from 'react';
    
    // 创建一个Context对象
    const ThemeContext = createContext();
    
    // 使用Provider提供Context的值
    function App() {
      const [theme, setTheme] = useState('light');
    
      return (
        <ThemeContext.Provider value={theme}>
          <Toolbar />
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </ThemeContext.Provider>
      );
    }
    
    // 消费Context的值
    function Toolbar() {
      // 使用useContext Hook访问Context的值
      const theme = useContext(ThemeContext);
    
      return <ThemedButton theme={theme} />;
    }
    
    function ThemedButton({ theme }) {
      return <button style={{ backgroundColor: theme === 'light' ? 'white' : 'black' }}>
        Button
      </button>;
    }
    
    export default App;

    在这个示例中,我们创建了一个ThemeContext来管理主题状态。在App组件中,我们使用ThemeContext.Provider来提供主题的值,并通过按钮来切换主题。在Toolbar组件中,我们使用useContextHook来访问主题的值,并将其传递给ThemedButton组件。这样,我们就实现了跨级传递数据的功能。

1.7 React 事件机制

React 的事件机制是指 React 对浏览器原生事件系统的一层封装和改进。其核心是 "合成事件" (SyntheticEvent),通过事件代理的方式将所有事件统一处理,从而提升性能和跨浏览器兼容性。

1)合成事件(SyntheticEvent):React 会将浏览器的原生事件包装成一个合成事件对象。这个对象具有与原生事件对象相同的接口,但跨浏览器兼容性更好。

2)事件代理:React 会将组件内的所有事件监听统一附加到最外层的 DOM 容器(通常是 document)。当事件发生时,由这个顶层节点捕捉事件并进行分发,而不是每个组件都各自处理事件。

React 的事件机制原理主要涉及到事件注册、事件合成、事件冒泡、事件派发等关键步骤,它虽然与原生 DOM 事件机制有所不同,但仍然是基于浏览器的事件机制完成的。以下是 React 事件机制原理的详细解释:

  1. 事件注册:
    • React 并不将事件直接绑定到具体的 DOM 节点上,而是统一绑定到 document
    • 当组件挂载时,React 会根据组件内声明的事件类型(如 onClickonChange 等),给 document 添加事件监听,并指定统一的事件处理程序 dispatchEvent
    • React 会将 React 组件内所有事件统一存放到一个对象(如 listenerBank)内,以便在触发事件时能够找到对应的方法去执行。
  2. 事件合成(SyntheticEvent):
    • React 使用 SyntheticEvent 作为所有事件的基类,该类定义了合成事件的基础公共属性和方法。
    • React 会根据当前的事件类型来使用不同的合成事件对象,如 SyntheticMouseEvent(鼠标事件)、SyntheticFocusEvent(焦点事件)等。
    • SyntheticEvent 是 React 对原生事件的封装,处理了浏览器兼容问题,并且具有与原生事件相同的接口,如 stopPropagation()preventDefault()
  3. 事件冒泡:
    • React 中的事件传播遵循与原生 DOM 事件相似的机制,包括捕获阶段和冒泡阶段。
    • 在 React 中,事件传播是通过组件的 props 来实现的。当一个组件触发一个事件时,React 会调用该组件的事件处理函数,并将事件对象作为参数传递给该函数。
    • 默认情况下,React 中的事件处理程序在冒泡阶段被触发。如果需要处理捕获阶段的事件,可以使用如 onClickCapture 的事件处理函数。
  4. 事件派发(dispatchEvent):
    • React 通过 dispatchEvent 方法统一进行事件的派发。当事件触发时,React 会根据当前的组件 ID 和事件类型找到对应的事件回调函数并执行。
    • React 中的事件传播可以用于实现组件之间的通信和交互,如父组件可以通过 props 将事件处理函数传递给子组件,在子组件中触发该事件时,父组件可以捕获到该事件并执行相应的逻辑。
  5. 其他特点:
    • React 中的事件对象 e 是一个合成对象,而不是原生的事件对象。这个对象已经被 React 封装过,并且处理了浏览器兼容问题。
    • React 的事件命名方式采用驼峰命名法(如 onClick),与 DOM 的命名方式(如 onclick)有所不同。
    • 在 React 中,不能通过返回 false 来阻止默认行为或阻止事件冒泡。需要显式调用 e.preventDefault()e.stopPropagation() 来实现这些功能。

总结来说,React 的事件机制原理通过事件注册、事件合成、事件冒泡和事件派发等步骤实现了对事件的高效处理和管理。这种机制减少了内存消耗、提高了性能,并且为开发者提供了统一的规范和友好的开发体验。

1.8 React 异步机制

React的异步机制是其性能优化的关键部分,其底层逻辑主要涉及到调度器(Scheduler)、协调器(Reconciler)以及Fiber架构。以下是对React异步机制底层逻辑的详细解释:

  1. 调度器(Scheduler)

    调度器是React中负责任务调度的部分。它根据任务的优先级来决定任务的执行顺序,确保高优先级的任务能够优先得到执行。这种机制使得React在面对大量更新时,能够保持界面的流畅性,避免卡顿。

  2. 协调器(Reconciler)

    协调器是React中负责对比新旧虚拟DOM(VDOM),并计算出最小DOM变化的部分。在React 16及以后的版本中,协调器采用了Fiber架构来实现异步更新。

  3. Fiber架构

    Fiber是React的最小工作单元,它代表了一个可以中断和恢复的更新过程。Fiber架构使得React能够将更新过程拆分成多个小的任务,并在浏览器的空闲时间中逐个执行这些任务。这种机制避免了长时间的阻塞操作,提高了应用的响应性

    具体来说,Fiber架构通过以下方式实现异步更新:

    1. 双树结构:Fiber架构中维护了两棵树,一棵是当前渲染的树(current Fiber Tree),另一棵是正在内存中构建的树(workInProgress Fiber Tree)。这两棵树通过alternate属性互相指向,方便在更新完成后进行切换。
    2. 任务拆分:React会将更新过程拆分成多个小的任务(即Fiber节点),每个任务都包含了一定的更新逻辑。这些任务被放入一个任务队列中,等待调度器进行调度。
    3. 时间片分配:调度器会根据任务的优先级和浏览器的空闲时间,为每个任务分配一个时间片。在时间片内,React会尽可能多地执行任务,直到时间片用完或任务队列为空。
    4. 中断与恢复:如果在一个时间片内任务没有执行完,React会将当前任务的状态保存到内存中,并等待下一个时间片继续执行。这种机制使得React能够在浏览器的空闲时间中逐步完成更新过程,而不会阻塞其他操作。
  4. MessageChannel

    React Scheduler 使用 MessageChannel 的目的就是为了产生宏任务。为了实现:

    1. 将主线程还给浏览器,以便浏览器更新页面。
    2. 浏览器更新页面后继续执行未完成的任务。
  5. 异步更新的优势

    1. 提高性能:通过拆分任务和分配时间片,React能够充分利用浏览器的空闲时间进行更新操作,避免了长时间的阻塞操作,提高了应用的性能。
    2. 优化用户体验:异步更新机制使得React能够更快地响应用户的输入和操作,减少了界面的卡顿和延迟,提高了用户体验。
    3. 支持高优先级任务:调度器能够根据任务的优先级进行调度,确保高优先级的任务能够得到优先执行,进一步提高了应用的响应性和稳定性。

综上所述,React的异步机制通过调度器、协调器和Fiber架构的协同工作,实现了高效的异步更新过程。这种机制不仅提高了应用的性能,还优化了用户体验,使得React在面对大量更新时能够保持界面的流畅性和稳定性。

1.9 React 生命周期

React 的生命周期主要分为三个阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。每个阶段都有对应的生命周期函数。

  1. 挂载阶段(Mounting):组件被创建并插入到 DOM 中。
    • constructor(): 这个函数是类组件的初始化方法。可以在这里初始化状态或者绑定事件处理方法。
    • static getDerivedStateFromProps(): 在每次渲染之前都会调用这个生命周期方法,允许组件基于接收到的属性更新内部状态。它是静态方法,因此无法访问 this
    • render(): 这是一个纯函数,根据组件的状态和属性,返回要渲染的 JSX 内容。
    • componentDidMount(): 组件已经被插入到 DOM 中。这里适合进行 DOM 操作或者发起网络请求。
  2. 更新阶段(Updating):组件的状态或属性发生变化时,会重新渲染。
    • static getDerivedStateFromProps(): 与挂载阶段中的作用相同。
    • shouldComponentUpdate(): 在渲染之前调用,可以让你控制组件的更新。默认返回 true,如果返回 false,组件就不会更新。
    • render(): 执行实际的渲染操作。
    • getSnapshotBeforeUpdate(): 在最新的渲染结果提交到 DOM 之前调用,可以捕获当前 DOM 的状态,比如滚动位置。
    • componentDidUpdate(): 组件的更新已经反映到 DOM 中,这里可以进行 DOM 操作或者网络请求。注意,要避免在这里直接调用 setState() 引发无休止的循环更新。
  3. 卸载阶段(Unmounting):组件即将被从 DOM 中移除。
    • componentWillUnmount(): 组件将要被从 DOM 中移除。这个函数通常用于清理资源,比如取消网络请求或移除事件监听器。

新版本 React (16.3 之后) 引入了一些新的生命周期方法:

  • static getDerivedStateFromError()componentDidCatch(): 错误边界相关的生命周期方法,用于捕获子组件渲染时发生的错误。

React Hooks: 如果你使用函数组件,可以通过 hooks 来模拟生命周期方法:

  • useEffect():可以模拟 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个阶段。
  • useState():可以管理组件的状态。

所以,对于用 hooks 的函数组件,整个生命周期的流程还是可以被很好的模拟和处理的,只是方式不同。

1.10 React 组件通信

在 React 应用中,组件通信是构建复杂界面的基础。根据组件之间的关系(父子、兄弟、跨层级),有不同的通信策略。

通信场景通信方向推荐方式核心思路 / 代码示例注意事项 / 适用场景
父 → 子单向(数据下行)props<Child name={user} /> 子组件:props.name最基础、最常用;props 不可在子组件中直接修改
子 → 父单向(事件上行)父组件传递回调函数父定义 handleData → 传给子;子调用 props.onData(data)数据自下而上,符合单向数据流
兄弟组件双向(间接)状态提升共同父组件管理 state,通过 props 分发给两个兄弟兄弟之间不直接通信,通过父组件“搭桥”
跨多层组件自上而下(跳过中间组件)ContextcreateContext + useContextProvider 提供值,深层组件 useContext 消费避免“props 钻取”;适合主题、语言、用户信息等全局数据
父组件调用子组件方法父 → 子(命令式)ref + useImperativeHandle父用 ref 获取子组件实例,调用子暴露的方法(如 focus()用于非典型场景:控制焦点、滚动、播放器等
全局共享状态任意方向(跨任何组件)状态管理库(Redux / Zustand / MobX / Jotai)创建 store,任何组件通过 Hook 读写全局状态大型应用、多个无关组件需要共享频繁变化的数据
任意组件(不推荐)任意(事件驱动)事件总线(如 mitteventemitter3emitter.on('event', handler) / emitter.emit('event', data)破坏了单向数据流,难以调试;仅在集成非 React 库时谨慎使用
表单场景数据传递双向绑定(模拟)受控组件(value + onChange 回调)value={val} onChange={e=>setVal(e.target.value)}本质是“父→子(prop) + 子→父(回调)”的语法糖

1.11 React 错误边界 Error Boundary

1.12 key

在 React 中,key 是一个特殊的属性(prop),主要用于识别哪些元素改变了、被添加了或被删除了。它帮助 React 在更新列表(或同一位置的一组兄弟元素)时,高效地匹配新旧虚拟 DOM 树,从而减少不必要的 DOM 操作。

  • 标识列表项的唯一性
  • 优化渲染性能(diff 算法的依据)
  • 维持组件状态(避免状态错乱):这是最容易出 bug 的地方。如果使用不当(比如用数组索引作为 key),当列表顺序变化或增删时,React 可能会错误地复用组件实例,导致内部状态(如输入框的值、复选框选中状态)被保留在不正确的元素上。

1.13 mixins

mixins 是 React 早期为了解决组件间逻辑复用而引入的方案,现在已被 React 官方全面废弃(React 团队已经在 React 16 版本中完全移除了对 Mixins 的支持)。

  1. Mixins 是什么

    简单来说,mixins 是一种将可复用代码注入到 React 组件中的技术。在 React 使用 React.createClass 方法的时期,它曾被用来解决横切关注点(cross-cutting concerns)问题,例如日志记录、权限校验等。

  2. 为什么 Mixins 被淘汰

    核心缺陷说明
    命名空间冲突当多个 mixins 或组件自身定义了同名方法 (如 componentDidMount) 时,会发生覆盖,导致意想不到的错误,且难以调试。
    隐式的依赖关系mixins 内部可能依赖组件的特定方法或属性,但这种依赖是隐式的。在复用 mixin 时,不清楚它对宿主组件有何要求,导致代码脆弱且难以维护。
    状态来源不清晰mixins 可以向组件添加状态,当组件引入了多个 mixins 时,一个 this.state.xxx 的来源会变得模糊不清,极大增加了代码的理解和维护成本。
    导致“雪崩式”的复杂度起初一个简单的 mixin 可能会随着需求增加而膨胀,最终演变成一个“上帝对象”。当多个这样的 mixin 组合在一起时,组件的复杂度会呈指数级增长,难以理解和维护。
  3. 现代化替代方案

    方案核心思想适用场景
    高阶组件 (HOC)它是一个函数,接收一个组件作为参数,并返回一个增强后的新组件。适合逻辑复用性高、对渲染没有侵入性的场景,例如为组件注入全局属性、进行权限校验等。
    Render Props它是指一个组件通过一个值为函数prop 来动态决定自己要渲染什么。适合需要将内部状态或逻辑暴露给父组件进行渲染控制的场景,例如处理鼠标位置、窗口尺寸等逻辑。
    React Hooks从组件中抽离状态逻辑,使其可被独立测试和复用,而无需改变组件的层级结构。这是目前最主流、最推荐的方案,适合绝大多数逻辑复用场景,如处理副作用、订阅、表单状态管理等。

1.14 setState

在 React 中,setState() 函数通常被认为是异步的,这意味着调用setState() 时不会立刻改变 React 组件中 state 的值,setState 通过触发一次组件的更新来引发重绘,多次 setState 函数调用产生的效果会合并。

调用 setState 时,React 会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。这将启动一个称为和解(reconciliation)的过程。和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新 UI。 为此,React 将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较。

  1. 在 React 中,setState 的执行时机依赖于它所在的执行环境。具体来说:

    • React Fiber 的影响: 在新的 React Fiber 架构中,异步渲染提供了更精细的控制,使得 setState 的行为更加高效。Fiber 允许中断渲染和继续执行,更好地调配资源,这也是为什么 setState 的更新有时表现为异步。
    • 异步情况:当 setState 在 React 的生命周期方法(如 componentDidMountcomponentDidUpdate)或者合成事件(如捕获用户操作的事件处理函数)中被调用时,setState 是异步的。这种异步性是为了提高性能,避免频繁渲染和批处理多个 setState 调用。
    • 同步情况:当 setState原生事件处理函数(如 addEventListener 注册的回调)或者定时器(如 setTimeoutsetInterval)中被调用时,setState 是同步的。这是因为这些回调并不在 React 的合成事件系统或者生命周期管理之中。
  2. 执行机制和实现原理

    在 React 中,setState 是一个用于更新组件状态的函数。其执行机制可以归纳为以下几个重要步骤:

    • 1)单向数据流setState 触发状态更新,从而引起组件重新渲染。React 采用单向数据流,即状态从父组件流向子组件。
    • 2)合并更新:React 会对多次调用 setState 进行合并,批量处理更新,从而提升性能。
    • 3)异步更新:在某些情况下(比如事件处理函数),setState 是异步的,以便批量处理多个状态更新请求。
    • 4)触发重新渲染:每次状态更新都会引起组件及其子组件的重新渲染,除非 shouldComponentUpdateReact.memo 返回 false
    • 5)调用生命周期方法:状态更新会触发组件的生命周期方法(如 componentWillUpdatecomponentDidUpdate)。

    以上是 setState 的最关键执行机制。

  3. 批处理机制

    React 通过批处理机制优化多次状态更新,比如在事件处理函数中,连续调用 setState,它们并不会立即触发组件重新渲染,而是会被合并成一次更新。这是因为 React 会给事件处理过程加个标志,以实现批处理。

    更新队列:React 内部维护了一个更新队列,每次调用 setState,新的状态会被加入到更新队列中。然后在适当的时候,React 会批量处理这个队列中的状态变化,从而减少不必要的重新渲染。

1.15 childContextTypes

childContextTypes 是 React 的一个遗留 API,用于在类组件中实现跨组件的数据传递childContextTypesgetChildContext 方法一起使用,父组件通过定义这两个属性,将一些数据放在 context 上,然后子组件可以通过 context 对象访问这些数据。

主要作用有两个:

  1. 跨层级组件通信。
  2. 提升代码复用性。

这样父组件和深层次的子组件间可以共享一些全局数据,比如主题(theme)、当前用户信息、语言设置等,而无需逐层传递 props。

其实,这部分内容在 React 的新文档中已经不建议继续使用了,因为它存在一些局限性和隐患,比如:改变 context 的值不会自动触发子组件的重新渲染。因此 React 推出了新的 Context API,来替代传统的上下文机制,它更加简单和合理。

2. React 进阶

2.1 调用 super(props) 的目的

在 React 构造函数中调用 super(props) 是为了确保我们能够在子类中成功调用父类的构造函数,并能在构造函数中通过 this.props 正确访问到传入的 props。简而言之,它确保了继承的正确性和 props 的初始化

2.2 直接修改 state 有什么问题

在 React 中,通过 this.state 直接修改 state 是不推荐的:

  1. 绕过 React 的管理机制:直接修改 state 会绕过 React 的状态管理逻辑,导致 React 无法追踪和应用这些改动。
  2. 无法触发渲染:React 依赖 state 的改变来重新渲染组件,直接修改 state 不会启动这种过程,所以 UI 不会更新。
  3. 调试困难:直接修改 state 后,调试过程会变得更加复杂,因为无法通过 React 开发工具看到实时变化。

2.3 React 中如何监听 state 的变化

在 React 中,通常监听 state 的变化是通过在组件内使用生命周期方法(如 componentDidUpdate)或使用 useEffect 钩子来实现的。

2.4 如何操作虚拟 DOM 的 class 属性

在 React 中,为了操作虚拟 DOM 的 class 属性,一般会使用 className 属性,而不是直接操作 class 属性。React 使用 className 来代替传统的 HTML 属性 class,这是因为 class 是 JavaScript 的保留字。

jsx
<div className="my-class">Hello, World!</div>

2.5 render 函数的原理

在 React 中,render 函数是 React 组件生命周期的一个重要部分,它的主要作用是描述组件的 UI(即其展示逻辑)。React 会根据 render 函数返回的描述来生成并更新真实的 DOM 元素,从而通过最小的代价实现高效的 UI 更新。具体步骤包括:

  • 1)组件初次渲染时,React 会调用 render 函数生成虚拟 DOM 结构,即一棵由 React 元素(对象)组成的树。
  • 2)React 进一步将虚拟 DOM 转换为真实 DOM,并将其插入页面。
  • 3)在组件的状态(state)或属性(props)变化时,render 函数会重新执行,产生一个新的虚拟 DOM。
  • 4)React 使用一种叫做 "Diffing" 的算法,对比新旧虚拟 DOM,找出变化的部分。调和是 React 应用 Diff 算法更新组件的过程。React 在调和过程中可以确保只修改必要的部分,从而最大程度地减少 DOM 操作,提高性能。
  • 5)最终,React 根据这些差异,最小化更新真正的 DOM,实现高效渲染。

2.6 render 函数的触发

React 的 render 函数一般在以下几种情况下会被触发:

  • 首次渲染:当组件首次加载到 DOM 中时会触发 render。
  • 状态更新:当组件的 state(状态) 发生变化时。
  • 属性变化:当组件的 props(属性)发生变化时。
  • 父组件重新渲染:当父组件的 render 函数被触发时,即使当前组件的 props 和 state 没有变化,也会重新渲染。这个情况经常会导致一些不必要的重复渲染,为此,我们可以使用一些优化手段,如 React.memo、PureComponent 或 shouldComponentUpdate。

2.7 render 函数返回的数据类型

在 React 中,render 函数返回的是一个 React 元素(React Element)。React 元素是用来描述你希望在屏幕上看到的内容,它可以是一个普通的 HTML 元素,也可以是一个自定义的组件类型。

  1. React Element:这是 Render 函数的主要返回值。React 元素是一个普通的对象,它表示一个 UI 片段。有了这个元素,React 会根据它来构建虚拟 DOM,然后再根据虚拟 DOM 更新真实 DOM。通过调用 React.createElement 或使用 JSX 语法,我们可以创建这个元素。
  2. Component 或 Fragment:render 函数不仅可以返回原生 HTML 元素,还可以返回自定义组件或 React.Fragment。Fragment 是 React 用来返回多个子元素而不需要添加额外节点的解决方案。
  3. Array 或 null:在一些情况下,render 函数可以返回一个数组(包含多个 React 元素),甚至可以返回 null 表示不渲染任何内容。

2.8 render 方法中能访问 refs 吗

在 React 中,不推荐在 render 方法中直接访问 refs 的 current(例如 this.refs.someRef.current 或回调 ref)。主要原因是 refs 的赋值时机与 render 的执行阶段不匹配

  1. 赋值时机问题(类组件与函数组件)

    • 类组件:ref 是在组件挂载完成后componentDidMount)或更新后才被赋值的。render 执行时,组件尚未挂载到真实 DOM,因此 ref.current 的值为 null
    • 函数组件:useRef 返回的 ref 对象在每次渲染中保持引用不变,但 ref.current 也是在 DOM 更新后(即 useEffect 执行前)才被设置的。在 render 函数体中直接读取 ref.current,得到的仍然是旧值或 null
  2. Render 应该是纯函数

    React 要求 render(函数组件的函数体)保持纯净——不应有副作用,也不应依赖不稳定的外部状态。而 ref 的赋值过程本身就是一种副作用(与 DOM 交互),在 render 中读取 ref.current 会隐式依赖渲染后的 DOM 状态,违反了函数式原则,可能导致难以预测的 bug。

  3. 正确做法:何时访问 refs

    如果需要读取或操作 ref 对应的 DOM 节点,应在以下生命周期或 Hook 中进行:

    • 类组件componentDidMountcomponentDidUpdate
    • 函数组件useEffectuseLayoutEffect(注意 useLayoutEffect 会在 DOM 更新后同步执行,适合读取布局信息)。

2.9 为什么 props 被认为是只读的

React 中的 props 被认为是只读的,因为这有助于确保组件的纯函数性质,使其只依赖于输入的 props 和自身的状态来生成 UI,而不去修改 props 的值。这样可以提高代码的可预测性和可维护性,避免对外部状态的意外更改,从而帮助开发者创建出更加稳定和可调试的应用。

  1. 纯函数和不可变性: React 追求函数式编程风格,其中纯函数是非常重要的一部分。一个纯函数的输出只依赖于输入参数,而且不修改输入参数的值。props 的不可变性确保了父组件和子组件之间的数据传递是单向的,从而避免了副作用和复杂的状态管理。
  2. 单向数据流: React 强调单向数据流,在父组件往子组件传递数据时,通过 props 实现的。这种单向数据流能让数据的变化流程更加清晰和可控。父组件在任何时候都可以信赖其子组件不会改变传递下去的 props
  3. 状态管理: 在 React 中,有一种明确的区分:props 是外部传入的,不可变的;而 state 是组件内部维护的,是可变的。通过这种分离,React 能有效地管理组件状态和属性,增强代码的清晰度。
  4. 性能优化: 因为 props 是不可变的,React 可以在比较 props 是否变化的时候简单地用引用比较来提高性能。如果 props 是可变的,这种引用比较将无法进行,从而需要深度比较,这会增加性能开销。
  5. 调试和测试: 不可变的 props 极大地简化了调试和测试工作。因为 props 保持不变,开发者可以更容易地预测组件的行为,编写测试用例和进行调试。
  6. 特殊情况:函数和对象作为 props: 虽然 props 是只读的,但有时候需要传递函数或对象引用做为 props。此时需要格外小心避免函数或对象内部的副作用。例如,可以使用不可变数据结构或开始使用像 Redux 这样的状态管理工具来进一步维护状态和逻辑的独立性。
  7. 设计模式:不可变数据: 这个思想不仅适用于 React,它是不可变数据设计模式的一部分。许多现代前端框架和库,如 Redux、Immutable.js 等,都推崇不可变数据以提升性能和代码的可维护性。

2.10 PureComponent 下引用类型修改值页面不渲染

解决在 React 的 PureComponent 下引用类型修改值时页面不渲染的问题,最重要的是理解 PureComponent 如何决定是否重新渲染。 PureComponent 通过浅比较(shallow comparison)来判断 props 和 state 是否改变,若改变则重新渲染。如果我们直接修改引用类型(如对象或数组)的值,这些修改不会导致新的引用,从而无法触发重新渲染。因此,解决方案是在修改引用类型时,创建新的引用

具体的解决方案有:

  • 对于对象类型的 state 或 props,使用 Object.assign 或者解构赋值来创建新对象。
  • 对于数组类型的 state 或 props,可以使用 concat、slice 或者展开展开运算符 ... 来创建新数组。

2.11 constructor 和 getInitialState 的区别

  • getInitialState:是 ES5 时代 React.createClass专门用来定义初始 state 的方法。
  • constructor:是 ES6 类组件中的标准构造函数,可以初始化 state,也可以做其他事情(如绑定 this)。
方面getInitialStateconstructor
所属 APIReact.createClassclass Component extends React.Component
当前状态已废弃,React 16+ 不再推荐使用仍然可用,但不是必须(可以用类属性)
定义 state 的方式返回一个对象:return { count: 0 }直接赋值:this.state = { count: 0 }
能否接收 props可以,通过函数参数 getInitialState(props)可以,通过 constructor(props) 接收
方法自动绑定 this✅ 是(所有方法自动绑定)❌ 否(需要手动 .bind(this) 或使用箭头函数)
额外职责只能做 state 初始化还可以绑定事件处理函数、初始化 ref 等
现代替代方案无(已废弃)类属性 state = {...} 或 函数组件 useState

2.12 受控组件和非受控组件的区别

在 React 中,处理表单元素(如 <input><textarea><select>)时,有两种方式:受控组件和非受控组件。它们的核心区别在于表单数据由谁控制

受控组件:值由 React state 驱动;非受控组件:值由 DOM 自己维护,React 在需要时“问”一下 DOM。

维度受控组件非受控组件
数据源React 组件的 stateDOM 元素自身
值获取方式通过 value 属性绑定 state,onChange 更新 state通过 refdefaultValue,需要时从 DOM 读取
控制权React 完全控制输入框的值DOM 自己维护值,React 只负责“读取”
实时响应每次输入都会触发重新渲染,可实时验证/格式化只在需要时(如提交)获取值,不干扰输入过程
实现复杂度稍复杂(要写状态和事件处理)简单(无需为每个输入写 onChange)
代码可预测性高(单一数据源,调试方便)低(数据隐藏在 DOM 中)

如何选择?

场景推荐方案原因
需要实时验证(如密码强度、邮箱格式)受控组件每次输入都能运行验证逻辑,即时反馈
需要动态控制值(清空、填充默认值、根据其他输入联动)受控组件只需修改 state,视图自动同步
简单的无交互表单(如搜索框提交)非受控组件代码更少,性能稍好(无频繁重渲染)
非 React 代码集成(如第三方 jQuery 插件操作表单)非受控组件让 DOM 自己管理值,React 只读取最终结果
文件上传 <input type="file">非受控组件(必须)文件输入的值是只读的,只能通过 ref 获取文件列表

2.13 如何防止 HTML 被转义

在 React 中,如果要防止 HTML 被转义,可以使用 dangerouslySetInnerHTML 属性。这是 React 提供的替代浏览器 DOM 中的 innerHTML 属性的方案。

3. React 拓展

3.1 函数式编程是什么?有哪些应用场景?

函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算机程序视为数学中的函数计算,强调函数的输入输出是确定的,不会受到外部状态的影响。函数式编程一般采用无副作用、不可变数据等特性,使得代码更加模块化、可复用、易于测试和推理等。

函数式编程中的函数是一等公民,即可以像其他类型的数据一样被传递、赋值、返回等。函数式编程也强调函数的高阶抽象能力,通过组合和柯里化等技巧实现复杂的功能。

函数式编程适用于需要处理大量数据的场景,例如数据分析、机器学习、图像处理等。在这些场景中,函数式编程的无副作用、不可变数据等特性可以提高程序的性能和稳定性。函数式编程还适用于需要编写高度可复用的代码库、需要构建高层次抽象的系统等场景。

除此之外,函数式编程还可以用于实现异步编程、事件驱动编程、响应式编程等,它们都是函数式编程的应用场景。例如在 JavaScript 中,Promise 和 RxJS 都是基于函数式编程的思想实现的。

总之,函数式编程是一种强调函数抽象、模块化、不可变性等特性的编程范式,适用于处理大量数据、实现高度可复用代码库、构建高层次抽象的系统等场景。

3.2 函数式编程中的无副作用

函数式编程的一个核心概念是无副作用(Referential Transparency),也就是函数的执行只依赖于输入参数,不会对外部环境产生影响。副作用指的是函数执行过程中对外部环境进行了修改或产生了其他可观察的行为,比如修改全局变量、写入文件、发送网络请求等。

函数式编程强调无副作用的好处在于:

  1. 可靠性:由于函数的结果仅取决于输入参数,相同的输入永远得到相同的输出,不会受到外部环境的变化影响,因此更容易推断和验证函数的行为。
  2. 可测试性:函数无副作用意味着可以更容易地进行单元测试,只需关注输入和输出,而无需关心函数内部的状态变化。
  3. 可组合性:无副作用的函数更容易进行组合,通过将多个函数连接在一起,形成更复杂的功能,而不需要担心它们之间的相互影响。

然而,并非所有的程序都可以完全避免副作用,因为现实世界中很多任务都需要与外部环境进行交互。在函数式编程中,通常会将具有副作用的操作封装在纯函数的边界之外,以保持主要的业务逻辑的纯洁性。

函数式编程并不是要完全消除副作用,而是通过限制和管理副作用的方式来提高程序的可靠性、可测试性和可组合性。

3.3 React 应用的生产环境中,怎么定位到具体错误代码行数

在 React 应用的生产环境中,要定位具体的错误代码行数,可以采取以下几种方法:

  1. 使用 Source Map
    • 在构建生产版本时生成 Source Map 文件。Source Map 是一个映射文件,可以将压缩后的代码映射回原始的开发代码,包括行数、列数等信息。
    • 当生产环境中出现错误时,浏览器会根据 Source Map 将错误定位到原始代码的行数和列数,从而帮助你准确定位问题所在。
  2. 错误边界(Error Boundary)
    • 在 React 应用中使用错误边界来捕获并处理组件中的错误。你可以在错误边界的 componentDidCatch 方法中记录错误信息,包括错误的堆栈信息。
    • 在错误信息中包含堆栈信息,可以帮助你定位到具体出错的组件和代码行数。
  3. 使用监控和日志工具
    • 集成监控和日志工具,如 Sentry、Bugsnag 等,这些工具可以捕获前端错误,并提供详细的错误信息,包括出错的文件、行数等。
    • 通过这些工具提供的错误报告,你可以快速定位到具体的代码行数,并了解错误发生的上下文信息。
  4. 手动添加调试信息
    • 在代码中添加额外的调试信息,例如使用 console.log 输出变量的值或标识代码执行到了哪个阶段。
    • 这种方法虽然相对简单,但需要手动添加代码,并且可能会影响代码的性能和可读性。
  5. 使用浏览器开发者工具
    • 在浏览器的开发者工具中,可以查看 JavaScript 控制台中的错误信息,并通过点击错误信息来跳转到具体的代码行数。
    • 这种方法适用于快速定位到错误发生的地方,但可能无法精确定位到原始的开发代码行数,特别是在代码经过压缩和混淆的情况下。

综上所述,结合使用 Source Map、错误边界、监控工具和浏览器开发者工具,可以帮助你在 React 应用的生产环境中准确定位到具体的错误代码行数。

3.4 React 应用中实现页面切换时保存数据不丢失并记忆滚动条位置

在 React 应用中实现页面切换时保存数据不丢失并记忆滚动条位置,可以采取以下几种优化策略,以提升用户体验并减少页面闪烁:

1、使用 React Router 中的 <Route> 组件的 key 属性:

<Route> 组件中设置唯一的 key 属性,使得当路由切换时,React 不会重新渲染相同的组件,而是复用已经挂载过的组件,从而保留组件的状态和数据。

jsx
<Route key={location.pathname} path="/" exact component={Home} />
<Route key={location.pathname} path="/about" component={About} />

2、在组件中使用 React.memo 进行优化:

对于纯函数组件,可以使用 React.memo 进行包裹,以避免不必要的重新渲染。这样可以确保页面切换时只有必要的组件会重新渲染,减少页面闪烁。

jsx
const MemoizedComponent = React.memo(MyComponent);

3、使用 window.sessionStoragewindow.localStorage 进行数据存储:

在组件卸载前将需要保存的数据存储到 sessionStoragelocalStorage 中,在组件重新挂载时再从中恢复数据。这样可以确保数据不会在页面切换时丢失。

4、记忆滚动条位置:

  • 在页面切换时,监听 windowscroll 事件,将滚动条位置保存到状态或 sessionStorage 中。
  • 在页面重新加载或切换回来时,从状态或 sessionStorage 中恢复滚动条位置,使得用户可以无缝地恢复到之前的滚动位置。

5、使用 React 的 useEffect 钩子进行数据保存和恢复:

在组件中使用 useEffect 钩子监听组件的卸载和挂载事件,在卸载前将需要保存的数据存储起来,在挂载时再从存储中恢复数据。

jsx
useEffect(() => {
  // 在组件卸载前保存数据
  return () => {
    // 在组件重新挂载时恢复数据
  };
}, []);

通过以上优化策略,可以在页面切换时保持数据不丢失并记忆滚动条位置,同时减少页面闪烁,提升用户体验。

3.5 React 中的性能优化有哪些常见的技巧

3.6 React 的代码编写规范

React 代码编写规范主要包括以下几个方面:

  1. 组件命名:组件名应该采用大驼峰命名法(PascalCase),如MyComponent,方便区分普通的HTML元素与React 组件。
  2. 文件结构:遵循组织良好的文件结构,通常每个组件一个文件,并且文件名与组件名保持一致,例如:MyComponent.js
  3. JSX 语法:JSX 中嵌入 JavaScript 代码时,使用小括号包裹代码。避免使用纯 JavaScript 拼接字串形成的方式。保持 JSX 清晰可读。
  4. 状态和属性:避免在组件中直接修改 state 和 props,应该使用setState方法来更新 state,props 是只读的,不能修改。
  5. 无状态组件:对于仅依赖 props 渲染的组件,使用函数组件而不是类组件,因为函数组件更轻量且易于测试。
  6. PropTypes:对组件的 props 进行类型检查,使用PropTypes确保传递到组件的属性类型正确,有助于提高代码的健壮性和可维护性。
  7. 事件处理:事件处理函数应命名为动词短语,例如:handleClickonSubmit,并且应使用箭头函数或者bind绑定this上下文。
  8. 避免重复代码:提取公共逻辑和样式到单独的函数或组件中,遵循DRY(Don't Repeat Yourself)的原则。
  9. Linting:使用 ESLint 标准规范化代码风格,通过 linting 工具确保代码的一致性和质量。

3.7 SSR 和 RSC 的区别

3.8 在 React 中如何进行静态类型检测

在 React 项目中进行静态类型检测的最佳实践是使用 TypeScript。TypeScript 是 JavaScript 的超集,它为 JavaScript 添加了静态类型检查机制。通过使用 TypeScript,你可以提前捕获许多类型相关的错误,从而提高代码的可靠性和可维护性。

  1. 安装 TypeScript 及相关依赖

    bash
    npm install typescript @types/react @types/react-dom
  2. 创建 tsconfig.json

    在项目根目录下创建一个 tsconfig.json 文件,用于配置 TypeScript 编译器选项。一个基本配置如下:

    json
    {
      "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "jsx": "react",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["src"]
    }
  3. 重命名文件扩展名

    将你的 React 组件和其他文件从 .js / .jsx 扩展名改为 .ts / .tsx

  4. 添加类型注解

    在你的 React 组件及其他代码中逐步添加类型注解。比如,你可以在函数组件中添加类型注解:

    tsx
    import React from 'react';
    
    interface Props {
      name: string;
      age: number;
    }
    
    const Greeting: React.FC<Props> = ({ name, age }) => {
      return (
        <div>
          Hello, my name is {name} and I am {age} years old.
        </div>
      );
    };
    
    export default Greeting;
  5. 配置 ESLint 和 Prettier

    配置 ESLint 和 Prettier 可以进一步保证代码风格和质量一致。你可以安装 eslint-config-airbnb-typescript 来提供一组默认规则:

    bash
    npm install eslint eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript

    创建 .eslintrc.js.eslintrc.json 文件,并进行相关配置:

    javascript
    module.exports = {
      parser: '@typescript-eslint/parser',
      extends: [
        'airbnb-typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:react/recommended',
        'plugin:prettier/recommended',
      ],
      parserOptions: {
        project: './tsconfig.json',
      },
      rules: {
        // 可以根据需要自定义 ESLint 规则
      },
    };
  6. 类型定义文件

    除了 @types/react@types/react-dom ,你还可能需要安装其他类型定义文件来支持你项目中使用的各种库,例如 react-router-dom 的类型定义文件:

    bash
    npm install @types/react-router-dom

3.9 如果 React 组件的属性没有传值,它的默认值是什么

如果一个 React 组件的属性没有传值,它的默认值是 undefined。这意味着组件内部如果直接访问这个属性会得到 undefined

  1. 设定默认属性值:defaultProps

    javascript
    // 类组件
    class MyComponent extends React.Component {
      render() {
        return <div>{this.props.text}</div>;
      }
    }
    MyComponent.defaultProps = {
      text: '默认文本'
    };
    
    // 函数式组件
    const MyComponent = ({ text }) => {
      return <div>{text}</div>;
    };
    MyComponent.defaultProps = {
      text: '默认文本'
    };
  2. 使用 ES6 解构赋值设定默认值

    javascript
    // 函数式组件
    const MyComponent = ({ text = '默认文本' }) => {
      return <div>{text}</div>;
    };
  3. PropTypes 校验

    javascript
    MyComponent.propTypes = {
      text: PropTypes.string
    };

    PropTypes 用于检查传入组件的属性类型是否与预期的类型匹配,有助于捕获传值阶段的潜在 BUG。

3.10 PropTypes 和 Flow 的区别

在 React 中,PropTypes 和 Flow 都是用于类型检查的工具,但它们的使用场景和功能略有不同。简要来说,PropTypes 是一种运行时的类型检查工具,而 Flow 则是一种静态类型检查工具。

  1. PropTypes
    • 运行时检查组件 props 的类型,确保传入的 props 符合预期。
    • 运行时检查:由于 PropTypes 进行的是运行时检查,只有在组件实际运行时才能检测出类型错误。这意味着类型错误的检测依赖于完整的测试覆盖率。
    • 易于配置:PropTypes 是 React 内置支持的,只需引入 prop-types 包并配置相应的类型声明即可。
    • 灵活性:PropTypes 可以对某些复杂的数据结构进行自定义验证,比如数组中的对象类型。
  2. Flow
    • 编译时进行类型检查,帮助开发者在编写代码时就能发现类型错误。
    • 编译时检查:Flow 是编译时进行类型检查的工具,因此在编写代码时就可以发现大部分类型错误,提升了开发效率。
    • 类型推断:相比 PropTypes 的显式类型声明,Flow 可以对一些类型进行自动推断,减少了手动类型声明的工作量。
    • 静态类型:Flow 提供了静态类型系统,可以支持整个项目的类型检查,而不仅仅是组件的 props。
  3. 如何选择:
    • 如果项目相对简单、团队规模较小或者你不想引入额外的构建工具,PropTypes 可能是更好的选择。
    • 如果项目复杂、代码基数较大、团队多人协作且对类型安全性有较高需求,Flow 则会带来更显著的优势。

3.11 如何定时更新一个 React 组件

要定时更新一个 React 组件,你可以使用 JavaScript 的 setInterval 方法结合 React 的生命周期方法(例如 useEffect)来实现。如果使用的是类组件,则可以使用 componentDidMountcomponentWillUnmount 来进行配置和清理定时器。简单来说,通过设置一个定时器,每隔固定的时间间隔去改变组件的 state,从而触发 re-render。

jsx
import React, { useState, useEffect } from 'react';

const TimerComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(interval); // 清理定时器
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
    </div>
  );
};

export default TimerComponent;

4. React Hooks

4.1 Hooks 中为什么不能写判断

  1. 挂载阶段:
    • 创建 Hooks 链表。
    • 执行链表。
  2. 更新阶段:
    • 执行链表 --> 校验是否可以执行。

4.2 useState

useState是React中的一个钩子(Hook),它允许我们在函数组件中引入状态(state)。useState返回一个数组,其中第一个元素是当前状态值,第二个元素是更新状态的函数。

使用useState来动态地管理和更新组件的状态。这样可以使我们的组件更加灵活和互动。

4.3 useEffect 和 useLayoutEffect 的区别

  • useEffect:DOM 更新前调用,微任务调用,页面渲染之后执行。适用于:纯数据处理。
  • useLayoutEffect:DOM 更新后调用,同步调用,页面渲染之前执行。适用于:数据修改之后要操作 DOM,防止重复渲染。

注意:DOM 更新 --> 页面渲染(顺序)。

4.4 useRef 和 ref 的区别

useRefref 在 React 中都是用于访问 DOM 元素或组件实例的工具,但它们在使用方式、适用场景以及实现原理上存在明显的区别。以下是关于 useRefref 的详细区别:

  1. 使用方式:
    • ref:在类组件中,通常使用 React.createRef() 方法来创建一个 ref 对象,并将其赋值给类的一个实例属性。然后,可以通过这个 ref 属性来访问对应的 DOM 元素或组件实例。在函数组件中,ref 不能直接使用,因为函数组件没有实例。
    • useRef:是 React 提供的一个 Hook,专门用于函数组件中。它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(通常是 null)。这个 ref 对象在整个组件的生命周期内保持不变,可以用来保存任何可变的值,不仅仅是 DOM 元素或组件实例。
  2. 适用场景:
    • ref:主要用于类组件中,通过 ref 可以访问到子组件的实例或 DOM 元素,从而进行一些需要直接操作 DOM 或组件实例的操作,如读取输入值、设置焦点等。
    • useRef:在函数组件中替代了 ref 的功能,并且功能更为强大。除了用于访问 DOM 元素或组件实例外,useRef 还可以用来保存一些不需要触发组件重新渲染的状态,如定时器、上一次的渲染结果等。此外,useRef 还可以用来绕过闭包问题,确保回调函数或定时器能够获取到最新的变量值。
  3. 实现原理:
    • ref:在类组件中,ref 是通过实例属性来实现的。在创建组件实例时,React 会将 ref 属性所指向的回调函数或 createRef() 创建的 ref 对象存储到组件实例上,从而可以在组件内部通过 this.refs 或直接访问 ref 对象来访问到对应的 DOM 元素或组件实例。
    • useRef:在函数组件中,由于没有实例,React 通过 useRef Hook 来实现 ref 的功能。useRef 返回一个可变的 ref 对象,这个对象在组件的整个生命周期内保持不变。当组件重新渲染时,useRef 会返回同一个 ref 对象,从而避免了不必要的重新创建和重新赋值。
  4. 注意事项:
    • 在使用 ref 时,需要注意不要在渲染过程中读取或修改 ref 的值,因为这可能会导致不可预测的结果。
    • useRef 返回一个可变的 ref 对象,但不应该在渲染过程中读取或修改 ref 的 .current 属性。通常,我们只在事件处理函数、生命周期方法或副作用(如 useEffect)中读取或修改 ref 的 .current 属性。

总结来说,useRefref 都是用于访问 DOM 元素或组件实例的工具,但它们在使用方式、适用场景以及实现原理上存在差异。在函数组件中,我们通常使用 useRef 来替代 ref 的功能。

4.5 useCallBack 和 useMemo 的区别

useCallbackuseMemo 是 React Hooks 中用于性能优化的两个重要工具。它们的主要区别在于用途、参数、返回值类型以及使用场景。以下是对两者区别的详细解释:

  1. 功能不同:

    • useCallback:用于记忆化(memoize)回调函数,避免在每次渲染时都重新创建相同的函数。它接受一个回调函数和一个依赖项数组作为参数,当依赖项发生变化时,会返回一个新的记忆化的回调函数。
    • useMemo:用于记忆化(memoize)计算结果,避免在每次渲染时都进行昂贵的计算。它接受一个计算函数和一个依赖项数组作为参数,当依赖项发生变化时,会重新计算并返回一个新的记忆化的计算结果。
  2. 参数不同:

    • useCallback:接受一个回调函数和一个依赖项数组作为参数。
    • useMemo:接受一个计算函数和一个依赖项数组作为参数。
  3. 返回值类型不同:

    • useCallback:返回一个记忆化的回调函数。
    • useMemo:返回一个记忆化的计算结果。
  4. 使用场景不同:

    • useCallback:主要用于优化传递给子组件的回调函数,特别是当这些回调函数作为 props 传递,并且父组件频繁渲染时。通过使用 useCallback,可以确保子组件在 props 没有实际变化时不会重新渲染,从而提高性能。
    • useMemo:主要用于优化计算操作,特别是当这些计算操作涉及复杂的计算或数据转换,并且这些计算的结果在多次渲染之间不会发生变化时。通过使用 useMemo,可以避免在每次渲染时都执行昂贵的计算,从而提高性能。
  5. 示例

    • 使用 useCallback 优化回调函数:

      jsx
      const handleClick = useCallback(() => {  
        console.log('Button clicked!');  
      }, []); // 依赖项数组为空,表示没有依赖项
    • 使用 useMemo 优化计算结果:

      jsx
      const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

总结来说,useCallbackuseMemo 都是用于性能优化的 React Hooks,但它们的用途、参数、返回值类型和使用场景有所不同。在实际开发中,可以根据具体的需求和场景选择使用哪个 Hook 来提高应用的性能。

5. 虚拟 DOM

5.1 真实 DOM 和虚拟 DOM 的区别

真实 DOM(Document Object Model)和虚拟 DOM 是前端开发中两个重要的概念,它们的核心区别在于它们的工作原理。

  • 真实 DOM 是浏览器直接操作的 DOM 对象,当你需要更新页面中的某个元素时,浏览器会直接修改该元素的内容,这样的操作是即时的。
  • 虚拟 DOM 是一种抽象形式的 DOM,它是由 React 提出的。虚拟 DOM 是一个 JavaScript 对象,它代表了真实 DOM 的结构变化。在进行更新时,React 首先在虚拟 DOM 中执行,然后对比新旧虚拟 DOM,然后将变化应用到真实 DOM。

5.2 为什么使用虚拟 DOM

React使用虚拟DOM来提高性能是因为直接操作真实DOM是相对昂贵和耗时的。虚拟DOM是一种轻量级的副本,它允许React在内存中进行计算和差异检测,然后将最小的、更高效的变化应用到真正的DOM上。这样可以减少不必要的真实DOM更新,提高应用的整体性能。

  1. 真实DOM的性能瓶颈
    • 浏览器的真实DOM操作(如添加、删除、修改节点)是动画流畅度的天敌。每次操作都要引起浏览器的重排和重绘,这是非常耗时的过程。
    • 当频繁进行这些操作时,会严重影响网页的性能和用户体验。
  2. 虚拟DOM的基本概念
    • 虚拟DOM(Virtual DOM,简称VDOM)是对真实DOM的一种抽象,一般表现为一个轻量级的JavaScript对象,包含元素的属性、事件以及子节点等信息。
    • 有点类似于真实DOM的影子,能在内存中快速生成和更新。
  3. 虚拟DOM的工作方式
    • 在React应用中,当状态改变时,React会首先在内存中的虚拟DOM树计算出新的虚拟DOM。
    • 然后React会对新旧虚拟DOM进行对比,找出有改动的部分,这个过程叫做“diffing”。
    • 最后,React会将差异部分高效地更新到真实DOM,尽量减少对真实DOM的操作,提升性能。
  4. 优势
    • 性能提升: 减少直接操作真实DOM的次数,降低性能开销。
    • 跨平台支持: 虚拟DOM的抽象性使得React不仅可以在浏览器中使用,还能够扩展到移动端(比如React Native),甚至是虚拟现实环境中。
    • 简化开发逻辑: 将开发者从手动管理DOM中解放出来,专注于状态和UI的设置和更新。
  5. 快速例子
    • 假设我们在React组件中更新一个列表项。如果直接操作真实DOM,对列表中的每个项都进行操作需要高昂的计算成本。
    • 使用虚拟DOM,React会生成新的虚拟DOM树,检查所有项发现只有一个需要更新,然后只更新这个项而不影响其他部分。

6. React 底层原理

6.1 Fiber 架构

6.2 MessageChannel

在React中,MessageChannel是一个Web API,它允许我们在不同的浏览上下文(如窗口、iframe或Web Worker)之间建立通信管道。这个API对于React应用来说,虽然不直接用于组件的状态管理或渲染,但在某些特定的异步通信场景中可能会非常有用。以下是对React中异步MessageChannel的详细解释:

  1. MessageChannel的基本概念

    MessageChannel接口创建了一个新的消息通道,这个通道有两个端口(port1port2),它们可以互相发送消息。这些消息是以DOM Event的形式发送的,因此它们是异步的。

  2. MessageChannel在React中的应用场景

    1. 跨窗口或iframe通信
      • 在React应用中,如果需要在主窗口和iframe之间传递消息,MessageChannel可以提供一个安全且高效的方式。
      • 父窗口可以创建一个MessageChannel,并将port2通过postMessage方法传递给iframe。然后,父窗口和iframe就可以通过各自的端口进行双向通信。
    2. Web Worker通信
      • Web Worker是在后台线程中运行JavaScript代码的一种方式,它不会阻塞主线程。
      • 在React应用中,如果需要使用Web Worker来处理耗时任务,MessageChannel可以用于在主线程和Worker之间传递消息。
      • 需要注意的是,由于MessagePort是可传输对象(Transferable Objects),因此它可以在postMessage方法中被传递,从而在Worker中获得并使用。
  3. 使用MessageChannel的注意事项

    1. 安全性
      • 在使用MessageChannel进行跨窗口或iframe通信时,要确保只向受信任的上下文传递端口。
      • 避免在不受信任的上下文中暴露过多的能力,以防止潜在的安全风险。
    2. 资源管理
      • 在通信结束后,应主动调用close方法来断开MessagePort的连接,以便回收资源。
      • 如果不再需要MessageChannel,可以考虑将其删除或置为null,以避免内存泄漏。
    3. 兼容性
      • 尽管MessageChannel在现代浏览器中得到了广泛的支持,但在使用之前仍应检查浏览器的兼容性。
      • 对于不支持MessageChannel的浏览器,可以考虑使用其他通信方式(如localStoragesessionStorage或第三方库)作为替代方案。
  4. 示例代码

    以下是一个简单的示例,展示了如何在React应用中使用MessageChannel进行跨窗口通信:

    javascript
    // 父窗口代码
    const { port1, port2 } = new MessageChannel();
    
    // 监听来自iframe的消息
    port1.onmessage = (event) => {
      console.log('Received message from iframe:', event.data);
    };
    
    // 将port2传递给iframe
    const iframe = document.querySelector('iframe');
    iframe.contentWindow.postMessage('Hello from parent!', '*', [port2]);
    
    // iframe代码(假设在另一个HTML文件中)
    window.onmessage = (event) => {
      if (event.ports.length > 0) {
        const port = event.ports[0];
    
        // 监听来自父窗口的消息
        port.onmessage = (event) => {
          console.log('Received message from parent:', event.data);
    
          // 向父窗口发送消息
          port.postMessage('Hello from iframe!');
        };
      }
    };

    在这个示例中,父窗口创建了一个MessageChannel,并将port2传递给了一个iframe。然后,父窗口和iframe通过各自的端口进行双向通信。

总的来说,MessageChannel在React应用中可以用于处理特定的异步通信场景。然而,对于大多数React应用来说,使用React的状态管理机制(如useStateuseReducer等)和React的上下文(Context)API来处理组件之间的通信可能更为常见和直观。