前端面试-React

2022/6/9 InterviewReact

# React的class和hooks

react 17版本之前

主要分为三个阶段:初始化阶段、更新阶段、卸载组件。

  1. 初始化阶段:也称为组件挂载时的阶段,这个阶段会执行我们在初次加载组件到组件第一次渲染在界面上面期间的一系列钩子函数。

    执行的流程为:constructor->componentWillMount->render->componentDidMount

    • constructor(props, context):这是一个构造器,这里面可以接收一个父组件传来的props然后初始化state值。
    • componentWillMount(): void:组件将要挂载,这个是在执行render之前会执行这个函数,也就说会在渲染浏览器DOM之前执行这个函数。
    • render(props, state, context): ComponentChild:在这里面我们会写一些html标签及自定义的函数,render执行完后便会将这些语句对应渲染到浏览器上面。
    • componentDidMount(): void:组件挂载完毕执行,也就在render执行完之后之后,浏览器的DOM树已经有了,所以我们便可以在这里操作DOM和ref。通常在这个钩子函数里面我们请求一些后端接口数据,来初次渲染我们页面。
  2. 更新阶段:在更新了state值的时候或者是接收到父组件props值的时候。

    这个阶段常规流程是:componentWillReceiveProps->shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate

    • componentWillReceiveProps(nextProps, nextContext):当子组件收到父组件传过来的props,会执行这个函数。
    • shouldComponentUpdate(nextProps, nextState, nextContext): boolean:当更新state值的时候会执行这个函数,比如this.setState({})
    • componentWillUpdate(nextProps, nextState, nextContext): void:执行render前的一个钩子函数,在react17中将要弃用这个钩子,执行this.forceUpdate()可以直接从这个钩子函数节点开始执行。
    • render:和初始化时候执行的那个render一样,只是这里是更新值的,所以DOM节点会重新更新一下。
    • componentDidUpdate(previousProps, previousState, snapshot): void:组件更新完毕执行的钩子函数。
  3. 卸载组件:当组件卸载时执行的钩子函数,这里只有一个,那就是componentWillUnmount,一般我们在这个函数里面关闭一些定时器或其他收尾的操作。

react 17版本之后(包括)

在新的生命周期中,react弃用了componentWillMountcomponentWillReceivePropscomponentWillUpdate这三个钩子,取而代之的是static getDerivedStateFromProps(props, state),其实就是把那三个钩子的含义融入到了这一个钩子中,react会在初始挂载和后续更新时调用render之前调用它。它应该返回一个对象来更新state,或者返回null就不更新任何内容。写法如下:

static getDerivedStateFromProps(props, state) {
  // 每当当前用户发生变化时,
  // 重置与该用户关联的任何 state 部分。
  // 在这个简单的示例中,只是以 email 为例。
  if (props.userID !== state.prevUserID) {
    return {
      prevUserID: props.userID,
      email: props.defaultEmail
    };
  }
  return null;
}
1
2
3
4
5
6
7
8
9
10
11
12

另外还新增了一个钩子,getSnapshotBeforeUpdate(prevProps, prevState),会在React更新DOM之前时直接调用它。它使你的组件能够在DOM发生更改之前捕获一些信息(例如滚动的位置)。此生命周期方法返回的任何值都将作为参数传递给componentDidUpdate的第三个参数。

Hook是React16.8的新增特性。它可以让你在不编写class的情况下使用state以及其他的React特性。在Hook中,我们写的都是函数组件,也就没有了类组件这些生命周期钩子,但是取而代之的是Hook提供的一些钩子,其含义也和类组件里面的生命周期函数类似。

  • useState - 用于添加状态到函数组件并在状态改变时重新渲染。
  • useReducer - 类似于useState,但用于管理复杂状态,具有dispatch函数。
  • useEffect - 用于处理副作用,类似于componentDidMountcomponentDidUpdate,可指定清理函数相当于componentWillUnmount
  • useContext - 用于访问React上下文的状态。先前的值和新的值会使用Object.is来做比较。使用memo来跳过重新渲染并不妨碍子级接收到新的context值。
  • useCallback - 用于记住回调函数的变化。
  • useMemo - 用于记住依赖于props的计算结果。
  • useRef - 用于访问组件内的DOM节点或保留某个值的变化。
  • useImperativeHandle - 自定义使用ref时的行为。
  • useLayoutEffect - 类似于useEffect,但在所有DOM变更后同步执行效果,可用于读取DOM布局和同步重新渲染。

# PureComponent和memo

都是React提供的用于减少组件重复渲染的组件,PureComponent适用于class组件,memo适用于函数组件。

import React, { PureComponent } from 'react';

class Child extends PureComponent { /*...*/ }
1
2
3

React.PureComponentReact.Component很相似。两者的区别在于React.Component并未实现shouldComponentUpdate(),而React.PureComponent中以浅层对比prop和state的方式来实现了该函数。如果赋予React组件相同的props和state,render()函数会渲染相同的内容,那么在某些情况下使用React.PureComponent可提高性能。

React.PureComponent中的shouldComponentUpdate()仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的props和state较为简单时,才使用React.PureComponent,或者在深层数据结构发生变化时调用forceUpdate()来确保组件被正确地更新。你也可以考虑使用immutable对象加速嵌套数据的比较。

此外,React.PureComponent中的shouldComponentUpdate()将跳过所有子组件树的prop更新。因此,请确保所有子组件也都是“纯”的组件。

如果一个PureComponent组件自定义了shouldComponentUpdate生命周期函数,则该组件是否进行渲染取决于shouldComponentUpdate生命周期函数的执行结果,不会再进行额外的浅比较。如果未定义该生命周期函数,才会浅比较状态state和props。

import React, { memo } from 'react';

const Child = () => { /*...*/ };
1
2
3

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
1

可选参数arePropsEqual:一个函数,接受两个参数:组件的前一个props和新的props。如果旧的和新的props相等,即组件使用新的props渲染的输出和表现与旧的props完全相同,则它应该返回true,否则返回false(这一点与shouldComponentUpdate刚好相反)。通常情况下,你不需要指定此函数。默认情况下,React将使用Object.is比较每个prop。

# useEffect和render谁先执行,父子组件useEffect执行顺序

在React中,‌当组件更新时,‌React会按照一定的顺序处理组件的更新过程。‌在这个过程中,‌useEffect的执行顺序是在render之前。‌具体来说,‌当React更新一个组件时,‌它会首先执行useEffect中的函数,‌这些函数通常用于处理副作用操作,‌如数据获取、‌订阅事件等。‌完成这些操作后,‌React才会进行渲染(‌render)‌过程,‌即生成虚拟DOM并与实际DOM进行同步。‌这种设计使得React能够在渲染前处理必要的副作用操作,‌从而确保组件状态的正确更新和视图的一致性。‌

react类组件写法下父子组件的生命周期执行顺序:

  1. constructor
  2. componentWillMount
  3. render
  4. constructor
  5. componentWillMount
  6. render
  7. componentDidMount
  8. componentDidMount

useEffect可以简单看作是componentDidMountcomponentDidUpdatecomponentWillUnmount的组合。react保证了每次运行effect的同时,DOM都已经更新完毕,故而,在react函数式组件写法的父子组件中,useEffect的执行顺序是:

  1. useEffect
  2. useEffect

# 组件通信,Redux的流程

通信方式:

  1. 父子组件通信:通过props传递信息。
  2. 兄弟组件通信:使用共享的上下文(Context)或者状态管理库(如Redux)。
  3. 非父子组件通信:使用自定义事件或者状态管理库。

Redux的工作流程主要包括以下几个步骤:‌

store.dispatch(action) => middleware? => reducer(action, prevState) => UI

  1. 创建Action:‌当用户与应用程序交互时(‌例如点击按钮)‌,‌会触发一个Action。‌Action是一个描述了发生了什么的普通JavaScript对象。‌它必须包含一个用于描述类型的type字段,‌并且可以通过Action Creator函数来创建Action。‌
  2. 触发Action:‌通过调用Redux的dispatch函数来触发Action,‌将Action发送给Redux的Store。‌
  3. 更新Store:‌Redux的Store接收到Action后,‌会将其传递给Reducer进行处理。‌Reducer是一个纯函数,‌用于根据Action的类型和数据更新Store中的状态。‌
  4. 更新View:‌当Store的状态发生变化时,‌Redux会通知相关的组件进行重新渲染,‌使得View与更新后的Store状态保持一致。‌
  5. 获取State:‌组件可以通过调用Redux的getState函数来获取当前的Store状态。‌
  6. 订阅State变化:‌Redux提供了subscribe函数,‌组件可以通过订阅来监听Store中状态的变化,‌当状态发生变化时执行相应的操作。‌

Redux中间件:

Redux中间件(Middleware)是Redux提供的一种机制,用于在Action被分发(Dispatch)到Reducer之前或Reducer处理完状态之后,插入自定义的逻辑。中间件可以用于处理异步操作、日志记录、错误处理等场景。

Redux中间件是一个函数,接收store作为参数,返回一个新的函数。这个新函数接收next作为参数,返回另一个函数。最终的这个函数接收action作为参数,可以在其中执行自定义逻辑。

import { createStore, applyMiddleware } from 'redux';

const middleware = (store) => (next) => (action) => {
  // 自定义逻辑
  console.log('Dispatching action:', action);
  const result = next(action); // 调用下一个中间件或 Reducer
  console.log('Next state:', store.getState());
  return result;
};

const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
1
2
3
4
5
6
7
8
9
10
11

中间件的执行顺序

  1. dispatch(action)被调用时,中间件会依次执行。
  2. 每个中间件可以决定是否继续传递action,或者直接返回结果。
  3. 最后一个中间件会调用next(action),将action传递给Reducer。

常见中间件:

  • redux-thunk:用于异步操作。它允许dispatch一个函数(称为Thunk),而不是普通的Action对象。
  • redux-saga:用于管理副作用(如异步操作)的中间件。
  • redux-logger:用于记录Action和状态变化的中间件。
  • redux-promise:用于处理Promise的中间件。它允许dispatch一个Promise,并在Promise解决后自动分发对应的Action。

# setState是同步还是异步的

setState是同步还是异步的

# hook为什么不能放在条件或循环里面

类组件的状态是以一个对象形式储存的,每个状态都有一个key和value相对应。而在函数式组件中,useState方法只接受了状态的初始值作为参数,并没有key,所以,函数式组件的状态不能以对象的形式存储,只能以线性表的形式存储,在内部用的是链表。React的函数式组件每次渲染都会重新生成状态,且每一次渲染都有一个状态序列,如果在条件语句里调用,就可能导致某次渲染的时候状态序列有缺失,从而出现异常。

# useEffect和useLayoutEffect的区别

  • useEffectuseLayoutEffect的主要区别在于它们的执行时机和对页面渲染的影响。useEffect在组件渲染完成后异步执行,不会阻塞页面的渲染;而useLayoutEffect在组件渲染完成后同步执行,可能会阻塞页面的渲染。
  • useEffect的回调函数在浏览器绘制完成后异步执行,即在组件渲染完成后执行。它不会阻塞浏览器的渲染工作,适合执行一些不紧急的副作用操作。
  • useLayoutEffect的回调函数在浏览器绘制前同步执行,即在组件渲染之后、浏览器布局之前执行。由于它是同步执行的,可能会阻塞浏览器的渲染,适合需要立即执行且不影响用户看到一致界面的操作。

一般来说,优先使用useEffect,因为它不会阻塞页面的渲染,适合处理大多数副作用操作。只有在需要在浏览器布局前立即执行代码且不会产生不良影响时,才考虑使用useLayoutEffect

# react-router里的Link标签和a标签的区别

  1. 功能:

    • Link:在单页应用程序(SPA)中提供导航,而不会导致页面重新加载。当用户点击链接时,React会阻止浏览器默认的页面刷新行为,并且使用react-router提供的导航方式,只更新URL并渲染对应的组件,从而实现单页面应用(SPA)的效果。。
    • a:单击时会导致完整页面重新加载,导航到新URL。
  2. 性能:

    • Link:由于不会导致页面重新加载,因此它提供更好的用户体验,特别是在SPA中。它提高了性能,因为避免了不必要的网络请求。
    • a:完整页面重新加载会导致较慢的用户体验,因为需要从服务器获取新页面。
  3. 无障碍:

    • Link:提供更好的无障碍性,因为它可以通过键盘聚焦和激活。
    • a:可能不那么无障碍,因为它不提供与按钮或其他交互式元素相同的键盘导航和焦点行为。

# React的Fiber架构

在React16之前,React使用Stack Reconciler进行组件树的更新和渲染。Stack Reconciler是一个同步的、递归的算法,存在以下问题:

  • 不可中断:一旦开始渲染,必须一次性完成,无法中断。
  • 性能瓶颈:对于复杂的组件树,渲染过程可能阻塞主线程,导致页面卡顿。
  • 优先级问题:无法区分高优先级任务(如用户交互)和低优先级任务(如数据加载)。

React的Fiber架构是React16引入的一种新的渲染机制,用于实现可中断的异步渲染。Fiber是React中的一种数据结构,用于描述组件树的结构和状态。Fiber架构的核心目标是提高React应用的性能和用户体验,特别是在处理复杂组件树和高优先级任务时。Fiber架构通过引入可中断的异步渲染和优先级调度,解决了这些问题。

核心概念:

  1. Fiber是React中的一种数据结构,用于描述组件树的结构和状态。每个Fiber节点对应一个组件或DOM节点,包含以下信息:

    • 组件的类型(如函数组件、类组件、原生DOM节点等)。
    • 组件的属性和状态。
    • 子节点和兄弟节点的引用。
    • 父节点的引用(即return指针)。
    • 当前节点的状态(如是否已完成渲染)。
    {
      type: 'div', // 节点类型
      key: null, // 节点的唯一标识
      child: null, // 第一个子节点
      sibling: null, // 下一个兄弟节点
      return: null, // 父节点
      alternate: null, // 副本节点
      effectTag: null, // 副作用标记
      stateNode: null, // 对应的 DOM 节点
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. 双缓冲机制:Fiber架构使用双缓冲机制来优化更新和渲染过程。每个Fiber节点都有一个alternate指针,指向它的副本。在更新过程中,React会在当前Fiber树和副本Fiber树之间切换,从而实现高效的更新。

  3. 优先级调度:Fiber架构引入了优先级调度机制,允许React根据任务的优先级决定渲染顺序。例如:

    • 高优先级任务(如用户输入)会优先处理。
    • 低优先级任务(如数据加载)可以延迟处理。

Fiber架构的工作原理:

  1. 可中断的异步渲染

    Fiber架构将渲染过程分解为多个小任务(称为Fiber节点),每个任务都可以独立执行和中断。React使用时间切片(Time Slicing)(浏览器的requestIdleCallback)在空闲时间执行这些任务,避免阻塞主线程。

  2. 任务调度 React根据任务的优先级调度渲染任务。高优先级任务会立即执行,低优先级任务可以延迟执行或中断。

  3. 增量渲染 Fiber架构支持增量渲染,即逐步完成组件树的渲染。React可以在完成部分渲染后暂停,处理高优先级任务,然后再继续渲染。

Fiber架构的作用:

  1. 提高性能

    • 通过可中断的异步渲染,Fiber架构避免了长时间阻塞主线程,提高了页面的响应速度。
    • 增量渲染和优先级调度机制进一步优化了渲染性能。
  2. 支持并发模式

    Fiber架构为React的并发模式(Concurrent Mode)奠定了基础。在并发模式下,React可以同时处理多个任务,并根据优先级动态调整渲染顺序。

  3. 更好的用户体验

    • 通过优先级调度,Fiber架构确保高优先级任务(如用户交互)能够及时响应,提升用户体验。
    • 增量渲染减少了页面卡顿,使应用更加流畅。

Fiber树的遍历是通过深度优先遍历(DFS)实现的。React会依次访问每个Fiber节点,并根据优先级调度渲染任务。

调度的工作原理

  1. 任务分解

    React将组件的更新和渲染任务分解为多个Fiber节点,每个节点对应一个小的任务单元。

  2. 优先级调度

    React根据任务的优先级调度任务执行顺序。优先级分为以下几类:

    • 同步优先级(Immediate):最高优先级,立即执行。
    • 用户阻塞优先级(User Blocking):高优先级,如用户交互。
    • 普通优先级(Normal):中等优先级,如数据加载。
    • 低优先级(Low):低优先级,如后台任务。
    • 空闲优先级(Idle):最低优先级,在空闲时间执行。
  3. 任务执行

    React使用双缓冲机制和增量渲染,逐步完成组件树的渲染。React可以在完成部分渲染后暂停,处理高优先级任务,然后再继续渲染。

  4. 任务中断与恢复

    React的任务是可中断的。当有更高优先级的任务需要处理时,React会中断当前任务,优先执行高优先级任务。高优先级任务完成后,React会恢复中断的任务。

调度的实现

  1. 调度器(Scheduler)

    React的调度器负责管理任务的优先级和调度。调度器使用小顶堆(Min Heap)数据结构存储任务,并根据优先级决定任务的执行顺序。

  2. 时间切片

    React使用requestIdleCallbackrequestAnimationFrame在浏览器的空闲时间执行任务,避免阻塞主线程。

  3. 优先级标记

    React为每个任务标记优先级,调度器根据优先级决定任务的执行顺序。

# react应用的三种模式

特性 Legacy Blocking Concurrent
渲染方式 同步渲染 同步渲染 并发渲染
优先级控制 部分支持 完全支持(SuspenseuseTransitionuseDeferredValue
兼容性 React16及之前版本 React17+ React18+
API ReactDOM.render ReactDOM.createBlockingRoot ReactDOM.createRoot
最近更新: 2025年03月10日 16:57:27