面试必背系列-React

295 阅读23分钟

1. Reac生命周期是怎样的?

生命周期

答:组件从 被创建 到 被销毁 的过程称为组件的生命周期。

生命周期

常见的生命周期。

  • constructor:该方法只会执行一次,组件的初始化工作,例如:初始化组件的 state。
  • render:是类组件中唯一必须实现的方法,它的返回值将作为页面渲染的视图。
  • componentWillReceiveProps:当从父类接收到 props 并且在调用另一个渲染器之前调用。
  • shouldComponentUpdate:这个方法用来判断是否需要调用 render 方法重绘 dom。返回true更新组件,返回false,阻止render渲染,默认返回true。
  • componentWillUpdate:在组件即将更新之前执行。(如果 shouldComponentUpdate 函数返回false,则不会调用 componentWillUpdate 方法。)
  • componentDidUpdate:在渲染发生后立即调用。
  • componentWillMount:在渲染之前执行。
  • componentDidMount:仅在第一次渲染后在客户端执行(发异步数据请求)。
  • componentWillUnmount:从 DOM 卸载组件后调用。用于清理内存空间。

新(react17)旧比较(废三增三)

  • 废弃了

    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  • 增加了

    • getDerivedStateFromError:在渲染阶段捕捉到了后代组件中的错误时会执行。
    • getDerivedStateFromProps:让组件在 props 变化时更新 state执行。
    • getSnapshotbeforeUpdate:此生命周期函数在最近一次渲染提交至 DOM 树之前执行,此时 DOM 树还未改变,我们可以在这里获取 DOM 改变前的信息,例如:更新前 DOM 的滚动位置。(它接收两个参数,分别是:prevProps、prevState,上一个状态的 props 和上一个状态的 state。)
面试大白话

答:首先,生命周期组件从 被创建 到 被销毁 的过程其常见的生命周期render阶段...commit阶段在react17有些改变其中废弃了3个生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。新增了getDerivedStateFromError、getDerivedStateFromProps、getSnapshotbeforeUpdate。

2. 虚拟DOM实现原理?

什么是Virtual DOM?

答:将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。

Virtual DOM的作用?

答:避免昂贵的DOM操作与大量无谓的计算量,通过diff算法,一边比较新旧节点一边给真实的DOM打补丁。

diff的原理?

React 分别对 tree diff、component diff 以及 element diff 进行算法优化。

  • tree diff

    • React 通过 深度优先 对 Virtual DOM 树进行层级控制,只会对相同 DOM 节点进行比较,即同一个父节点下的所有子节点。
    • 当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
  • component diff

    • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
    • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
    • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
  • element diff

    • 节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
diff的比较方式?

答:前端操作dom的时候了,不会把当前元素作为上一级元素或下一级元素,很少会跨越层级地移动DOM元素,在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较,因此复杂度的问题转换成 O(n) 复杂度的问题。

diff的弊端?

答:相同⽗元素的⼦元素必须有独特的 key,否则重复的 key 会造成渲染错误。

Vue vs React
  • 相同点

    • 都是两组虚拟dom的对比(react16.8之后是fiber与虚拟dom的对比)
    • 只对同级节点进行对比,简化了算法复杂度
    • 都用key做为唯一标识,进行查找,只有key和标签类型相同时才会复用老节点
    • 遍历前都会根据老的节点构建一个map,方便根据key快速查找
  • 不同点

    • react会自顶向下全diff.
    • vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
    • react在diff遍历的时候,只对需要修改的节点进行了记录,形成effect list,最后才会根据effect list 进行真实dom的修改,修改时先删除,然后更新与移动,最后插入
    • vue 在遍历的时候就用真实dominsertBefore方法,修改了真实dom,最后做的删除操作
    • react 采用单指针从左向右进行遍历
    • vue采用双指针,从两头向中间进行遍历
    • react的虚拟diff比较简单,vue中做了一些优化处理,相对复杂,但效率更高

React的请求应该放在哪个生命周期中?

  • 目前官方推荐的异步请求是在componentDidmount中进行.
  • 如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求

3. setState到底是异步还是同步?

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

4. React组件通信如何实现?

  • 父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
  • 子组件向父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
  • 兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
  • 跨层级通信: Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过
  • 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
  • 全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态

5. React如何进行组件/逻辑复用?

  • 高阶组件:

    • 属性代理
    • 反向继承
  • 渲染属性

    • render props
  • react-hooks

mixin、hoc、render props、react-hooks的优劣如何?(上一个问题展开)

  • Mixin

    • 缺点

      • 组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)

      • 多个 Mixin 之间可能产生冲突(比如定义了相同的state字段)

      • Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增

      • 隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:

        • 难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响
        • 组价自身的方法和state字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它
        • Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出
  • HOC

    • 优点

      • HOC通过外层组件通过 Props 影响内层组件的状态,而不是直接改变其 State不存在冲突和互相干扰,这就降低了耦合度
      • 不同于 Mixin 的打平+合并,HOC 具有天然的层级结构(组件树结构),这又降低了复杂度
    • 缺点

      • 扩展性限制: HOC 无法从外部访问子组件的 State因此无法通过shouldComponentUpdate滤掉不必要的更新,React 在支持 ES6 Class 之后提供了React.PureComponent来解决这个问题
      • Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题
      • Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本
      • 命名冲突: 如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性
      • 不可见性: HOC相当于在原有组件外层再包装一个组件,你压根不知道外层的包装是啥,对于你是黑盒
  • Render Props

    • 优点

      • 上述HOC的缺点Render Props都可以解决
    • 缺点

      • 使用繁琐: HOC使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props无法做到如此简单
      • 嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套
    • 简单的说,render props 将如何render组件的事代理给了使用它的组件,但同时以参数的形式提供了需要重用的状态和方法给外部。实现UI的自定义和功能的重用。

  • React Hooks

    • 优点

      • 简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁

      • 解耦: React Hooks可以更方便地把 UI 和状态分离,做到更彻底的解耦

      • 组合: Hooks 中可以引用另外的 Hooks形成新的Hooks,组合变化万千

      • 函数友好: React Hooks为函数组件而生,从而解决了类组件的几大问题:

        • this 指向容易错误
        • 分割在不同声明周期中的逻辑使得代码难以理解和维护
        • 代码复用成本高(高阶组件容易使代码量剧增)
    • 缺点

      • 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
      • 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
      • 破坏了PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
      • 在闭包场景可能会引用到旧的state、props值
      • 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
      • React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)

6. 你是如何理解fiber的?

  • React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.

  • Fiber:一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。

  • Fiber 节点拥有 parent, child, sibling 三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。

  • 其他答案

    • 概念

      • React Fiber 是一种基于浏览器的单线程调度算法.
    • 问题

      • React 在 V16 之前会面临的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于——更新流程是同步、不可中断的
      • React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
    • Fiber 架构怎么做的?

      • 让 React 渲染的过程可以被中断,可以将控制权交回浏览器,让浏览器及时地相应用户的交互——异步可中断
      • 通过将工作任务拆分成一个个工作单元分别来执行——Fiber
    • Fiber 即是一种数据结构,又是一个工作单位

      • React Fiber 机制的实现,就是依赖于下面的这种数据结构-链表实现的。其中每个节点都是一个 Fiber,一个 Fiber 包含了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)等属性。Fiber 节点中其实还会保存节点的类型、节点的信息(比如 state、props)、节点对应的值等
      • 将它视作一个执行单元,每次执行完一个“执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出来
    • 破解 JavaScript 中同步操作时间过长的方法其实很简单——分片。

7. 异步渲染有那两个阶段?

  • reconciliation

    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  • commit

    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount
  • 前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。

  • 因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。

  • getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用

  • getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

8. 你对 Time Slice的理解?

  • React 在渲染(render)的时候,不会阻塞现在的线程。如果你的设备足够快,你会感觉渲染是同步的。如果你设备非常慢,你会感觉还算是灵敏的。

  • 虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来。

  • 同样书写组件的方式,时间分片正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流畅运行。

  • 其他答案(参考)

    • 把一个耗时长的任务分成很多小任务,每一个小任务完成了,就把控制权交还给 React 负责任务协调的模块,看看有没有其他其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务

    • 如果一个任务还没完成(时间到了),就会被另一个更高优先级的更新过程打算,这个时候,优先级高的更新任务会优先处理,而低优先级更新任务所作的工作则会完全作废,然后等待机会重头再来。

    • React Fiber 更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase

      • 第一阶段,Fiber 会找到需要更新哪些 DOM,这个阶段可以被打算;但到了第二阶段,就会一鼓作气把 DOM 更新完,绝不会被打断

9. redux的工作流程?

  • 几个核心概念:

    • Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
    • State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
    • Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
    • Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
    • Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
    • dispatch:是View发出Action的唯一方法。
  • 工作流程

    • 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
    • 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
    • State一旦有变化,Store就会调用监听函数,来更新View。

10. react-redux是如何工作的?

  • Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store

  • connect: 负责连接React和Redux

    • 获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
    • 包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组 WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传 WrappedComponent
    • 监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

11. redux中如何进行异步操作?

  • 当然,我们可以在componentDidmount中直接进行请求无须借助redux.
  • 但是在一定规模的项目中,上述方法很难进行异步流的管理,通常情况下我们会借助redux的异步中间件进行异步处理.
  • redux异步流中间件其实有很多,但是当下主流的异步中间件只有两种redux-thunk、redux-saga,当然redux-observable可能也有资格占据一席之地,其余的异步中间件不管是社区活跃度还是npm下载量都比较差了.

12. redux异步中间件redux-thunk的优劣?

  • redux-thunk优点:

    • 体积小: redux-thunk的实现方式很简单,只有不到20行代码
    • 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单
  • redux-thunk缺陷:

    • 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
    • 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
    • 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装

13. 合成事件是什么?

  • React基于浏览器的事件机制自身实现了一套事件机制。包括事件注册、事件的合成、事件冒泡、事件派发等

  • React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)

  • 如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。

  • React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。

  • React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback

  • React 有一套自己的合成事件 SyntheticEvent

  • 其他答案

    • React 根据 W3C 规范定义了每个事件处理函数的参数,即合成事件。

    • 所有事件都挂在到document上

    • event不是原生的,是合成事件对象

      • 如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。
    • react为何要合成事件机制?

      • 更好的兼容性和跨平台
      • 挂在到document,减少内存消耗,避免频繁解绑 document,减少内存消耗,避免频繁解绑
      • 方便事件统一管理(如事务机制)

14. React Hooks 原理?

  • 修改核心是将useState,useEffect按照调用的顺序放入memoizedState中,每次更新时,按照顺序进行取值和判断逻辑,我们根据调用hook顺序,将hook依次存入数组memoizedState中,每次存入时都是将当前的currentcursor作为数组的下标,将其传入的值作为数组的值,然后在累加currentcursor,所以hook的状态值都被存入数组中memoizedState。先将旧数组memoizedState中对应的值取出来重新复值,从而生成新数组memoizedState。对于是否执行useEffect通过判断其第二个参数是否发生变化而决定的。
  • 这里我们就知道了为啥不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。因为我们是根据调用hook的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时useEffect第二个参数是数组,也是因为它就是以数组的形式存入的。

15. 为什么ReactHooks中不能有条件判断

  • 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  • 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。

16. 用过哪些 Hook

  • useState: 用于定义组件的 State,对标到类组件中this.state的功能

  • useEffect:通过依赖触发的钩子函数,常用于模拟类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法

  • 其它内置钩子:useContext: 获取 context 对象

  • useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux,并不是持久化存储,会随着组件被销毁而销毁;属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;配合useContext的全局性,可以完成一个轻量级的 Redux

  • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

  • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;

    • useMemo 与 useCallback 的区别

      • useCallback 是缓存了函数自身,而 useMemo 是缓存了函数的返回值。
  • useRef: 获取组件的真实节点;

  • useLayoutEffect:DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同。useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;可以获取更新后的 state;

  • 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。

17. Class 组件 VS Hook

  • 类组件与函数组件有什么异同?

    • 相同

      • 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。
    • 不同点:

      • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
      • 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
      • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
      • 从上手程度而言,类组件更容易上手,从未来趋势上看,由于React Hooks 的推出,函数组件成了社区未来主推的方案。
      • 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

18. 自定义过哪些 Hook

  • 防抖
  • 节流

19. React18有哪些更新?

答:新特性新APICM并发模式

新特性
scss
复制代码
// 1. setState 自动批处理
React 18 通过在默认情况下执行批处理来实现了开箱即用的性能改进。
(批处理: 在视图层,将多个渲染合并成一次渲染)
在 18 之前,只有在react事件处理函数中,才会自动执行批处理,其它情况会多次更新
在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次
arduino
复制代码
// 2. flushSync 退出批量更新
新的API

useId - 同一个组件在客户端和服务端生成相同的唯一的 ID。

useInsertionEffect - 提前注入新的style。

并发模式
  • 开启并发模式,

    • 1.默认使用批处理。
    • 2.使用并发特性API。 startTransition:将特定更新标记为“过渡”来显著改善用户交互(如搜索引擎的关键词联想)。

20. 说说对React refs 的理解?

Refs 在计算机中称为弹性文件系统(英语:Resilient File System,简称ReFS)

React 中的 Refs提供了一种方式,允许我们访问 DOM节点或在 render方法中创建的 React元素

本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点