likes
comments
collection
share

前端面试系列之 React 必备知识点

作者站长头像
站长
· 阅读数 13

之前整理了前端面试相关的知识,分享给大家!

注:很多内容来源于网络,侵权联系删除哈!

Reac生命周期是怎样的?

  • React 16之后有三个生命周期被废弃(但并未删除)

    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  • 目前React 16.8 +的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段

    • 挂载阶段:

      • constructor: 构造函数,最先被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this
      • getDerivedStateFromProps: static getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps
      • render: render函数是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
      • componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅
    • 更新阶段:

      • getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用
      • shouldComponentUpdate: shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能
      • render: 更新阶段也会触发此生命周期
      • getSnapshotBeforeUpdate: getSnapshotBeforeUpdate(prevProps, prevState),这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用
      • componentDidUpdate: componentDidUpdate(prevProps, prevState, snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。
    • 卸载阶段:

      • componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作

虚拟DOM实现原理?

  • 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
  • 状态变更时,记录新树和旧树的差异
  • 最后把差异更新到真正的dom中

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

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

setState到底是异步还是同步?

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

React组件通信如何实现?

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

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)

你是如何理解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 中同步操作时间过长的方法其实很简单——分片。

异步渲染有那两个阶段?

  • reconciliation

    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  • commit

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

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

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

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

你对 Time Slice的理解?

  • React 在渲染(render)的时候,不会阻塞现在的线程。如果你的设备足够快,你会感觉渲染是同步的。如果你设备非常慢,你会感觉还算是灵敏的。
  • 虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来。
  • 同样书写组件的方式,时间分片正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流畅运行。
  • 其他答案(参考)
    • 把一个耗时长的任务分成很多小任务,每一个小任务完成了,就把控制权交还给 React 负责任务协调的模块,看看有没有其他其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务
    • 如果一个任务还没完成(时间到了),就会被另一个更高优先级的更新过程打算,这个时候,优先级高的更新任务会优先处理,而低优先级更新任务所作的工作则会完全作废,然后等待机会重头再来。
    • React Fiber 更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase
      • 第一阶段,Fiber 会找到需要更新哪些 DOM,这个阶段可以被打算;但到了第二阶段,就会一鼓作气把 DOM 更新完,绝不会被打断

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。

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及其子组件的重新渲染

redux中如何进行异步操作?

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

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

  • redux-thunk优点:

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

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

合成事件是什么?

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

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

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

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

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

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

  • 其他答案

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

    • 所有事件都挂在到document上

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

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

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

React Hooks 原理?

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

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

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

用过哪些 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 这个特性,我们可以编写自定义钩子。

Class 组件 VS Hook

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

    • 相同

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

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

自定义过哪些 Hook

  • 防抖
  • 节流
  • ...more...