React常见面试题汇总

众所周知,React是目前互联网大厂使用最多的前端框架,在面试过程中,面试官也更喜欢有React开发经验的同学。本文总结了React中一些重要的特性,同时也是在平时的面试中经常被问到的一些问题。希望对各位前端开发同学有所帮助,如果觉得还不错请多多点赞收藏。

目录

  • 如何理解 Rect
  • React与Vue的异同
  • 如何理解虚拟 DOM
  • React 生命周期
  • React 状态复用的方式
  • 如何理解 React Hooks
  • React Diff 算法
  • React fiber 架构的理解
  • react 性能优化的手段
  • setState 是同步的还是异步的
  • React 合成事件

如何理解 Rect

  • react 是什么?

    react 是一个用于构建用户界面的 javascript 库。

  • 为什么会出现 react 这样的框架

    在我们早期的前端开发中,我们通过 JavaScript 来手动操作 DOM 以达到和用户交互的目的,但是原生的 DOM 操作繁琐并且还会存在兼容性问题。因此在我们前端开发中出现 jquery 这样的框架,来帮助我们简化 DOM 操作,同时解决了各种兼容性问题。但是随着我们的项目变得越来越复杂,我们发现,光有 DOM 操作远远不够,我们还要有应用结构,因此我们开始探讨前端组件化的问题,通过组件化的方式来让我们的项目结构变得更加清晰,同时也引入了 MVX 的概念。接着大家发现 MVX 的数据绑定依然需要我们手动监听 model 的变化,然后去做各种 DOM 操作,数据到视图层的映射依然繁琐。于是大家就开始推崇 MVVM 的数据绑定,于是就出现了例如 Angular,React 和 Vue 这样的前端框架。

  • react 的特点

    react具有声明式,组件化以及一次学习,随处编写的特点,因此 react 特别适合用于构建快速响应的大型 web 应用

React与Vue的异同

相同点:

  • 使用虚拟 DOM
  • 提供响应式和组件化的视图组件
  • 将注意力集中保持在核心库,而将其他功能如路由、全局状态管理交给相关的库

不同点:

  • 视图更新

    在react中,当某个组件的状态发生变化的时候,会以该组件为根,重新渲染整个组件树,如果要避免不必要的子组件重渲染,我们需要在所有可能的地方使用 PureComponent 或者是 shouldComponentUpdate 方法。

    在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以vue能精确知晓哪个组件确实需要被重渲染。Vue的特点可以让开发者不再考虑此类优化,从而更好地专注于应用本身

  • HTML 和 CSS

    在 react 中,everything is JavaScript,不仅是 html 可以用 jsx 表示,连 css 也有 js 的解决方案。

    Vue 的整体思想是拥抱经典的 Web 技术,并在其上进行扩展。使用 html,css 和 js 来构建模板。

    在这个方面我觉得 vue 是在适应开发者,而 react 想要改变开发者。

    同时,在组件作用域内的 css 上,react 是使用的 css-in-js 的解决方案或者是 css modules。而 vue 使用 scoped 来控制 css

  • 规模

    vue 的路由管理和状态管理由官方维护,而 react 则是把这些问题交给社区,创建了一个更为分散的生态系统,因此react的生态系统相对于 vue 更加繁荣。

  • 原生渲染

    由于两者都是使用的虚拟 dom 技术,因此都可以构建原生的安卓或者 ios 应用,例如 react-native 和 weex

如何理解虚拟 DOM

  • 虚拟 dom 是什么

    虚拟 DOM 是对 DOM 的抽象,本质上是 JavaScript 对象

  • 为什么需要虚拟dom

    首先,我们知道在前端性能优化中,有一个很重要的一项就是尽可能少的操作 DOM,不仅仅是 DOM 操作相对较慢,更是因为频繁的 DOM 操作会造成浏览器的回流或者重绘,这些都会对我们的性能造成影响。因此我们需要一个抽象,在patch过程中,尽量一次将差异更新到 DOM 中。

    其次,现代前端框架的一个基本要求就是无须手动操作DOM,这样可以大大提高开发效率

    最后,虚拟DOM可以更好地实现跨平台,用于编写原生应用。

React 生命周期

React < 16.3

组件的生命周期包含挂载或者更新组件过程中被触发的一系列方法。这些方法可以在组件渲染UI之前或者之后触发。以下是 React 16.3 之前的生命周期

图片说明

挂载生命周期

  • constructor(props):初始化 props 和 State。

  • componentWillMount()

    一旦属性被获取并且也初始化了 State,该方法将会被触发。该方法是在 DOM 被渲染之前触发的,并且可以用来初始化第三方脚本库、启动动画、请求数据,以及其他可能需要在组件被渲染之前执行的额外步骤。还可以在该方法中触发 setState 方法,在组件被初次渲染之前修改组件的 State。

    在组件被渲染完毕之前调用 setState 方法将不会启动更新生命周期。在组件渲染完毕之后调用 setState 方法就会启动更新生命周期。

    如果用户在 componentWillMount 方法中定义的回调函数内调用了 setState 方法,那么它将会在组件被渲染完成之后被触发,并且会启动更新生命周期

  • render()只要父组件 render 被调用,其所有子组件的 render 都会被调用

  • componentDidMount():组件被渲染完毕之后触发,可以获取到 DOM。

  • componentWillUnmount():组件被卸载之前触发,在这里可以清除一些后台进程。

更新生命周期

更新生命周期是当组件 State 发生变化或者从父组件接收到新的属性时触发的一系列方法。

更新生命周期会在每次调用 setState 方法后启动。调用 setState 方法过程内部的更新生命周期将会引发无限递归循环。因此只能在 componentWillReceiveProps 方法内部调用 setState 方法,它允许组件属性被更新后更新 State。

更新生命周期包括以下几个方法:

  • componentWillReceiveProps(nextProps)

    仅当新的属性被传递给了组件后才会调用。这也是唯一可以调用 setState 方法的地方

  • shouldComponentUpdate(nextProps, nextState)

    更新生命周期的门卫,一个可以取消更新操作的谓词。该方法可以通过只允许执行必须的更新操作来改进性能。

  • componentWillUpdate(nextProps, nextState)

    只在组件更新之前触发。和 componentWillMount 类似,只是它会在每次更新操作之前被触发。

  • componentDidUpdate(prevProps, prevState):

    只在更新操作发生后,调用 render 方法之后触发。类似 componentDidMount 方法,不过它会在每次更新之后触发。

React >= 16.4

React 大于等于 16.4 的生命周期图谱如下:

图片说明

还有一些不常用的生命周期钩子:

图片说明

这里我们着重介绍 getDerivedStateFromProps生命周期。需要注意的是,React 16.3 的版本中 getDerivedStateFromProps 的触发范围和 16.4^ 是不同的,主要区别在 setStateforceUpdate 时会不会触发。具体可以看生命周期图

该生命周期的使用场景主要有两个:

  • 无条件的根据 prop 来更新内部 state,也就是只要有传入 prop 值,就更新 state。
  • 只有 props 值和 state 不同时才更新 state。

具体示例可以参考官方文档

React 状态复用的方式

  • mixin

    • mixin之间的相互依赖会形成依赖链,当我们改动其中一个mixin的状态的时候,很可能会直接影响其他的mixin,不利于代码维护
    • 不同mixin的之间可能会产生命名冲突
    • 增加组件的复杂性,当我们引入的mixin较多时,会让代码的逻辑变得十分复杂,难以维护。
  • 高阶组件

    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  • render props

    render props是一项通过props来告知组件需要渲染什么内容的技术,它的使用场景是什么呢? 很多时候我们渲染一个组件,但是它的逻辑和数据却依赖于父组件,这种情况下我们可以把那部分可以复用的逻辑抽取在父组件中,并且在父组件暴露一个参数 render 来接收渲染子组件的方法,并且通过这个方法把子组件所依赖的数据传给它,这种方式就是render props。

    render props的使用思路是控制反转,子组件通过render props的方式定义好渲染自身的函数,父组件再把子组件需要的数据传给这个函数,子组件拿到数据后可以随意的渲染自身。

  • hooks

    • 减少状态逻辑复用的风险,mixin引入的状态逻辑会相互覆盖,HOC也可能会出现 props 覆盖
    • 避免层级嵌套,让组件更易于理解
    • 使用函数代替 class ,更符合 react 函数式理念

    问题

    • 只能在顶层使用 hooks
    • 只在 React 函数中使用 hooks

如何理解 React Hooks

  • 是什么

    hook 是 react16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 react 特性

  • 解决了什么问题

    react解决了我们在使用和维护 react 中经常遇到的问题

    • 组件之间的状态和逻辑复用

      以前使用 render props高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。

    • 复杂组件变得难以理解

      我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMountcomponentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

    • 难以理解的 class

      class 组件中经常需要手动去绑定 this,因此需要我们去理解 js 中 this 的工作原理,增加了学习成本

  • 有哪些

    • useState
    • useEffect
    • useRef
    • useContext
    • useMemo
    • useCallback

React Diff 算法

React的 diff 策略:

  • 在 Web UI 中,跨层级移动节点的操作很少,可以忽略不计
  • 拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构
  • 对于同一层级的一组子节点,可以通过唯一 id 进行区分

基于以上策略,React 分别对 Tree diff、Component diff 以及 Element diff 进行了优化。

tree dif

在 tree diff 的时候,react 只会对两颗树的同一层级的节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

如果出现了跨层级的移动,React会删掉该节点以及其所有的子节点。然后在新的节点下创建节点。

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A

只有删除、创建操作,没有移动操作

component diff

React 是基于组件构建的应用,因此在进行 diff 时,react 遵循以下策略:

  • 如果是同一类型的组件,则按照原策略继续比较 DOM tree
  • 如果是不同类型的组件,react 会替换整个组件下的所有子节点

对于同一类型的组件,其虚拟dom可能并没有发生变化,因此react提供shouldComponentUpdate来让我们判断该组件是否需要diff。

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。

element diff

当节点处于同一层级的时候,react diff 提供了三种节点操作,移动、删除、插入。

当节点没有 key 的时候,在对比新老虚拟DOM的时候,会出现频繁的创建,删除节点的操作,不能很好的做到节点复用,影响 diff 的效率。

因此我们在对同一层级的子元素进行比较的时候,需要添加唯一的key进行区分。

图片说明

有了 key 之后,react diff 在同层级比较的时候,

  • 首先会初始化 lastIndex 和 nextIndex,两者都为 0.
  • 遍历新的虚拟dom集合,找到新的集合中的每个节点在老集合中的位置 oldIndex,
    • 如果 oldIndex >= lastIndex,则该节点保持不动,并更新 lastIndex = Math.max(lastIndex, oldIndex),然后更新该节点的位置为 nextIndex,nextIndex ++
    • 如果 oldIndex < lastIndex,则移动该节点至 nextIndex 的位置,同时更新 lastIndex = Math.max(lastIndex, oldIndex),nextIndex ++
    • 如果老集合中没有找到节点,说明节点是新增的,则会创建新节点插入到 nextIndex 的位置
  • 当完成新集合中所有节点 diff 时,最后还需要对老集合进行遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

图片说明

优化 react diff 算法

例如以下两个新旧数组,React的算***把 a, b, c 移动到他们的相应的位置 + 1共三步操作,而inferno.js则是直接将d移动到最前端这一步操作.其中一个核心的思想就是利用LIS(最长递增子序列)的思想做动态规划,找到最小的移动次数.

 * A: [a b c d]
 * B: [d a b c]

React fiber 架构的理解

fiber 之前的 react存在的问题:

由于 js 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行的时候,另一个线程只能挂起等待。

如果 js 线程长时间占用了主线程,那么我们渲染层面的更新就不得不长时间的等待,界面长时间不更新,会导致页面响应变慢,用户可能会感觉卡顿。

这也正是 react15 所面临的问题,当 react 在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断。

如果组件较大,那么 js 线程会一直执行,然后等到整棵 vDOM 树计算完成后,才会交给渲染线程,这就有可能出现卡顿的现象。

因此 facebook 提出了 react fiber架构

fiber 把渲染更新过程拆分为多个子任务,其中优先级高的先执行,并且每次只做其中的一小部分,做在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。

上述实现的方式是window.requestIdleCallback() 方法,该方法在浏览器空闲时段内调用排队的函数

会导致多次调用某些生命周期钩子

react把更新分为两个阶段

  • reconciliation

    这个阶段的更新是可以被打断的,主要涉及的生命周期:

    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  • commit

    这个阶段的更新是不能被打断的

    • componentDidUpdate
    • componentDidMount
    • componentWillUnmount

react 性能优化的手段

  • 避免不必要的 render,主要是使用 shouldComponentUpdatePureComponent 或者 Reac.memo 实现,pureComponent 只做浅层比较,因此可以使用 immutable 的方式进行更新

  • 使用 useMemo 或 useCallback 缓存变量或者函数

  • 使用 Suspense 或者 lazy 进行组件的懒加载

    import React, { Suspense } from 'react';
    
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }

setState 是同步的还是异步的

主要取决于 react 当前是否处于 批量更新的模式,如果是,则会把更新存在一个队列里面,如果不是,则会直接执行此次更新。

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setStatesetState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

React 合成事件

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象。React 的合成事件它并不是绑定在 DOM 元素本身,而是统一绑定在 document 上。这样做主要有三个目的

  • 进行浏览器兼容,实现更好的跨平台
  • 事件对象可能会被频繁创建和回收,使用事件池来进行统一管理,达到性能优化的目的

合成事件与原生事件的执行顺序:先执行原生事件,再处理 React 合成事件

合成事件与原生事件的区别

  • 命名方式不同
  • 事件处理函数写法不同
  • 阻止默认行为方式不同
#春招##实习##笔试题目##面经##前端##学习路径#
全部评论
大佬图片挂了😭
点赞 回复
分享
发布于 2022-03-06 17:21
setState不是useState中的setState吧
点赞 回复
分享
发布于 2022-08-02 10:56
联易融
校招火热招聘中
官网直投
请教一下,React 合成事件(SyntheticEvent)在16后不是绑定在document上吧,是统一绑定在root上的吧
点赞 回复
分享
发布于 2022-09-26 10:10 河北

相关推荐

25 204 评论
分享
牛客网
牛客企业服务