【金三银四】React 面试常见问题

·  阅读 5833
【金三银四】React 面试常见问题

最近团队招人,所以稍微准备了一些基础的面试题,如果大家感兴趣可以留言我接着更新~~~

打个广告,饿了么企业订餐前端团队持续招人中~~~ 感兴趣的私我~~~

React 有哪几种创建组件的方式?有什么区别

React 有三种构建组件的方式

  • React.createClass
  • ES6 class
  • 无状态函数

React.createClass 是 React 最传统、兼容性最好的方法。该方法构建一个组件对象, 当组件被调用时,就会创建几个组件实例

ES6 class 方式和 createClass 类似,只是从调用内部方法变成了用类来实现。

无状态组件创建时始终保持一个实例,避免了不必要的检查和内存分配。

简述下 React 的生命周期?每个生命周期都做了什么?

React 的生命周期主要分为三个阶段:MOUNTING、RECEIVE_PROPS、UNMOUNTING

  • 组件挂载时(组件状态的初始化,读取初始 state 和 props 以及两个生命周期方法,只会在初始化时运行一次)
    • componentWillMount 会在 render 之前调用(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。)
    • componentDidMount 会在 render 之后调用
  • 组件更新时(组件的更新过程是指父组件向下传递 props 或者组件自身执行 setState 方法时发生的一系列更新的动作)
    • 组件自身的 state 更新,依次执行
      • shouldComponentUpdate(会接收需要更新的 props 和 state,让开发者增加必要的判断条件,在其需要的时候更新,不需要的时候不更新。如果返回的是 false,那么组件就不再向下执行生命周期方法。)
      • componentWillUpdate
      • render(能获取到最新的 this.state)
      • componentDidUpdate(能获取到最新的 this.state)
    • 父组件更新 props 而更新
      • componentWillReceiveProps(在此调用 setState,是不会触发 re-render 的,而是会进行 state 的合并。因此此时的 this.state 不是最新的,在 render 中才可以获取更新后的 this.state。
      • shouldComponentUpdate
      • componentWillUpdate
      • render
      • componentDidUpdate
  • 组件卸载时
    • componentWillMount(我们常常会在组件的卸载过程中执行一些清理方法,比如事件回收、清空定时器)

image.png

新版的生命周期函数增加了 getDerivedStateFromProps,这个生命周期其实就是将传入的 props 映射到 state 中。在 React 16.4 之后,这个函数每次会在 re-render 之前调用,

image.png

getDerivedStateFromProps的作用是

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

但是盲目使用这个生命周期会有一些问题

  • 直接复制 props 到 state 上
  • 如果 props 和 state 不一致就更新 state

DOM 真正被添加在 HTML 是在哪个生命周期?

componentDidMount、componentDidUpdate

state 更新之后发生了什么

state 更新之后,会依次执行 shouldComponentUpdate、componentWillUpdate、render 和componentDidMount。 shouldComponentUpdate 会接收需要更新的 props 和 state,让开发者增加必要的判断条件,在其需要的时候更新,不需要的时候不更新。如果返回的是 false,那么组件就不再向下执行生命周期方法。

setState 真的是异步的吗

setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和 hook() 的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,

当然可以通过第二个参数 setState(partialState, callback) 拿到更新后的结果。

setState 只在合成事件和 hook() 中是“异步”的,在 原生事件和 setTimeout 中都是同步的。

setState 之后发生了什么

简单版本: React 利用状态队列机制实现了 setState 的“异步”更新,避免频繁的重复更新 state。

首先将新的 state 合并到状态更新队列中,然后根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件。

复杂版本:

  1. enqueueSetState 将 state 放入队列中,并调用 enqueueUpdate 处理要更新的 Component
  2. 如果组件当前正处于 update 事务中,则先将 Component 存入 dirtyComponent 中。否则调用batchedUpdates 处理。
  3. batchedUpdates 发起一次 transaction.perform() 事务
  4. 开始执行事务初始化,运行,结束三个阶段
    1. 初始化:事务初始化阶段没有注册方法,故无方法要执行
    2. 运行:执行 setSate 时传入的 callback 方法
    3. 结束:更新 isBatchingUpdates 为 false,并执行 FLUSH_BATCHED_UPDATES 这个 wrapper 中的close方法,FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的 dirtyComponents,调用updateComponent 刷新组件,并执行它的 pendingCallbacks, 也就是 setState 中设置的 callback。

IMG_0189.jpg

在 shouldComponentUpdate 或 componentWillUpdate 中使用 setState 会发生什么?

当调用 setState 的时候,实际上会将新的 state 合并到状态更新队列中,并对 partialState 以及 _pendingStateQueue 更新队列进行合并操作。最终通过 enqueueUpdate 执行 state 更新。

如果在 shouldComponentUpdate 或 componentWillUpdate 中使用 setState,会使得 state 队列(_pendingStateQueue)不为 null,从而调用 updateComponent 方法,updateComponent 中会继续调用 shouldComponentUpdate 和 componentWillUpdate,因此造成死循环。

为什么不能直接使用 this.state 改变数据

setState通过一个队列机制来实现 state 更新。当执行 setState 的时候,会将需要更新的 state 合并后放入状态队列,而不会立刻更新 this.state。队列机制可以高效的批量更新 state,如果不通过 setState 而直接修改 this.state,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前被直接修改的 state,而造成无法预知的错误。

无状态组件没有 shouldComponentUpdate 方法怎么优化?

因为无状态组件是没有生命周期的,所以也就没有 shouldComponentUpdate。shouldComponentUpdate 是为了避免不需要的重新渲染。

所以无状态组件每次都会重新渲染。在性能上某些组件会有一些不必要的损耗。我们可以引用 Recompose 库的 pure 方法。(实际上 pure 方法就是将无状态组件转换为 class 语法在加上 PureRender 后的组件)

setState 之后做了什么,应该在什么时候使用 setState,在 componentWillMount、componentDidMount 中执行 setState

React DOM

如何将组件直接插入到 body 下

使用 protal, portal就是建立一个“传送门”,让 Dialog 这样的组件在表示层和其他组件没有任何差异,但是渲染的东西却像经过传送门一样出现在另一个地方。

在 v16 版本之前,是使用 unstable_renderSubtreeIntoContainer 这个 API 实现的。看名字中带有 unstable 就知道这是一个不鼓励使用的 api 但是为了解决问题,还是需要使用的。该 API 的作用和 render 类似,是更新组件到传入的 DOM 节点上,使用它我们就可以实现在组件内实现跨组件的 DOM 操作。

我们在源码中可以看到,unstable_renderSubtreeIntoContainer 方法和 render 的方法的区别只是 unstable_renderSubtreeIntoContainer 会传入一个父节点,而 render 该参数是 null。

在 v16 版本之后,可以在 render 的时候使用 createPortal 方法。

render() {
    return createPortal(
      <div class="dialog">
        {this.props.children}
      </div>, //塞进传送门的JSX
      this.node //传送门的另一端DOM node
    );
  }
复制代码

React 中 ref 是干嘛的?

使用 refs 获取。组件被调用时会新建一个改组件的实例。refs 会指向这个实例,可以是一个回调函数,回调函数会在组件被挂载后立即执行。

如果把 refs 放到原生 DOM 组件的 input 中,我们就可以通过 refs 得到 DOM 节点;如果把 refs 放到 React 组件中,那么我们获得的就是组件的实例,因此就可以调用实例的方法(如果想访问该组件的真实 DOM,那么可以用 React.findDOMNode 来找到 DOM 节点,但是不推崇此方法)。

refs 无法用于无状态组件,无状态组件挂载时只是方法调用,没有新建实例。在 v16 之后,可以使用 useRef。

react 的虚拟dom是怎么实现的

React 是把真实的 DOM 树转换为 JS 对象树,也就是 Virtual DOM。每次数据更新后,重新计算 VM,并和上一次生成的 VM 树进行对比,对发生变化的部分进行批量更新。除了性能之外,VM 的实现最大的好处在于和其他平台的集成。

比如我们一个真是的 DOM 是这样的

<button class="myButton">
  <span>this is button</span>
</button>
复制代码

那么在转化为 VM 之后就是这样的

{
  type: 'button',
  props: {
  	className: 'myButton',
    children: [{
      type: 'span',
      props: {
        type: 'text'
        children: 'this is button'
      }
    }]
  }
}
  
复制代码

为什么 VM 可以提高性能?

因为 VM 并不是真实的操作 DOM,通过 diff 算法可以避免一些不变要的 DOM 操作,从而提高了性能。

VM 一定会提高性能吗?

不一定,因为 VM 只是通过 diff 算法避免了一些不需要变更的 DOM 操作,最终还是要操作 DOM 的,并且 diff 的过程也是有成本的。

对于某些场景,比如都是需要变更 DOM 的操作,因为 VM 还会有额外的 diff 算法的成本在里面,所以 VM 的方式并不会提高性能,甚至比原生 DOM 要慢。

但是正如尤大大说的,这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需

简单介绍下 diff 算法

diff 算法主要有三个策略(观察的规律)

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

tree diff

因为上面的三个策略中的第一点, DOM 节点的跨级操作比较少,那么 diff 算法只会对相同层级的 DOM 节点进行比较。如果发现节点不存在 那么会将该节点以及其子节点完全删除 不会再继续比较。如果出现了 DOM 节点的跨层级的移动操作,那么会删除改节点以及其所有的子节点,然后再移动后的位置重新创建。

component diff

如果是同一类型的组件,那么会继续对比 VM 数 如果不是同一类型的组件,那么会将其和其子节点完全替换,不会再进行比对 同一类型的组件,有可能 VM 没有任何的变化,如果可以确定的知道这点,那么就可以节省大量的 diff 时间,所以用户可以设置 shouldComponentUpdate() 来判断是否需要进行 diff 算法。

element diff

当节点处于同一层级的时候时,有三种操作:INSERT_MAKEUP插入、 MOVE_EXISTING 移动、 REMOVE_NODE 删除 这里 React 有一个优化策略,对于同一层级的同组子节点,添加唯一的 key 进行区分。这样的话,就可以判断出来是否是移动节点。通过 key 发现新旧集合中的节点都是相同的节点,就只需要进行移动操作就可以。 当然 这里的优化策略和具体的 diff 方法还是很复杂的,就不展开写了。

react-router 里的 <Link> 标签和 <a> 标签有什么区别

对比 <a> 标签, Link 避免了不必要的重渲染

react-router是伴随着react框架出现的路由系统,它也是公认的一种优秀的路由解决方案。在使用react-router时候,我们常常会使用其自带的路径跳转组件Link,通过实现跳转;

react-router 接管了其默认的链接跳转行为,区别去传统的页面跳转,Link 的 “跳转” 行为只会触发相匹配的对应的页面内容更新,而不会刷新整个页面。

Link 跳转做了三件事情:

  1. 有onclick那就执行onclick
  2. click的时候阻止a标签默认事件
  3. 根据跳转 href,用 history 跳转,此时只是链接变了,并没有刷新页面

而 a 标签就是普通的超链接了,用于从当前页面跳转到href指向的另一个页面(非锚点情况)。

React 中的事件系统

简述下 React 的事件代理机制?

React 并不会把所有的处理函数直接绑定在真实的节点上。而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。

当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。

当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。

这样做的优点是解决了兼容性问题,并且简化了事件处理和回收机制(不需要手动的解绑事件,React 已经在内部处理了)。但是有些事件 React 并没有实现,比如 window 的 resize 事件。

React 的事件代理机制和原生事件绑定有什么区别?

  1. 事件传播与阻止事件的传播: React 的合成事件并没有实现事件捕获 只支持了事件冒泡。阻止事件传播 React 做了兼容性处理,只需要 e.preventDefault() 即可,原生存在兼容性问题。
  2. 事件类型:React 是 原生事件类型 的一个子集(React 只是实现了 DOM level3 的事件接口,有些事件 React 并没有实现,比如 window 的 resize 事件。)阻止 React 事件冒泡的行为只能用于 React 合成事件系统,但是 在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。
  3. 事件的绑定方式:原生事件系统中支持多种不同的绑定事件的方式,React 中只有一种
  4. 事件对象:原生中存在 IE 的兼容性问题,React 做了兼容处理。

React 的事件代理机制和原生事件绑定混用会有什么问题?

我们在平时的开发中应该尽可能的避免 React 的事件代理机制和原生事件绑定混用。

React 的合成事件层,并没有将事件直接绑定到 DOM 元素上,所以使用 e.stopPropagation() 来阻止原生 DOM 的冒泡的行为是不行的。阻止 React 事件冒泡的行为只能用于 React 合成事件系统,但是 在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。

React 中如果绑定事件使用匿名函数会怎么样

class Demo {
  render() {
    return <button onClick={(e) => {
      alert('我点击了按钮')
    }}>
      按钮
    </button>
  }
}

复制代码

这样的写法,因为使用的是匿名函数,所以组件每次都会认为是一个新的 props,不会使用缓存优化,在性能上会有一定的损耗。

React 组件间的通信

  • 父组件向子组件通信:props
  • 子组件向父组件通信:回调函数
  • 跨级组件通信:context
  • 没有嵌套关系的组件通信:eventEmitter,利用全局对象来保存事件,用广播的方式去处理事件。

性能优化

在 React 中可以做哪些性能优化?

  • 使用 shouldComponentUpdate 避免不需要的渲染,但是如果对 props 和 state 做深比较,代价很大,所以需要根据业务进行些取舍;在有子组件的情况下,为了避免子组件的重复渲染,可以通过父组件来判断子组件是否需要 PureRender。
  • 将 props 设置为数组或对象:每次调用 React 组件都会创建新组件,就算传入的数组或对象的值没有改变,他们的引用地址也会发生改变,比如,如果按照如下的写法,那么每次渲染时 style 都是一个新对象
// 不推荐
<button style={{ color: 'red' }} />

// 推荐
const style = { color: 'red' }
<button style={style} />

// 不推荐
<button style={this.props.style || {} } />  

// 推荐
const defaultStyle = {}
<button style={this.props.style || defaultStyle } />   
复制代码
  • 将函数的绑定移动到构造函数内:可以避免每次都绑定事件。
  • 使用 immutable 不可变数据,在我们项目中使用引用类型时,为了避免对原始数据的影响,一般建议使用 shallowCopy 和 deepCopy 对数据进行处理,但是这样会造成 CPU 和 内存的浪费,所以推荐使用 immutable,优点如下
    • 降低了“可变”带来的复杂度
    • 节省内存,immutable 使用结构共享尽量复用内存,没有被引用的对象会被垃圾回收
    • 可以更好的做撤销/重做,复制/粘贴,时间旅行
    • 不会有并发问题(因为数据本身就是不可变的)
    • 拥抱函数式编程
  • 给子组件设置一个唯一的 key,因为在 diff 算法中,会用 key 作为唯一标识优化渲染

为什么要给组件设置 key

在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新创建的还是被移动而来的元素,从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。

HOOKS

react hooks,它带来了那些便利

在没有 hooks 之前,我们使用函数定义的组件中,不能使用 React 的 state、各种生命周期钩子类组件的特性。在 React 16.8 之后,推出了新功能: Hooks,通过 hooks 我们可以再函数定义的组件中使用类组件的特性。 好处:

  1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
  2. 类定义更为复杂
  • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
  • 时刻需要关注this的指向问题;
  • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
  1. 状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

注意:

  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
  • 不能在useEffect中使用useState,React 会报错提示;
  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存

列举几个常见的 Hook

  • 状态钩子 (useState): 用于定义组件的 State,类似类定义中 this.state 的功能
  • 生命周期钩子 (useEffect): 类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。
  • useContext: 获取 context 对象
  • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
  • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
  • useRef: 获取组件的真实节点;

React Hooks当中的useEffect是如何区分生命周期钩子的

useEffect可以看成是> componentDidMount,> componentDidUpdate和> componentWillUnmount三者的结合。useEffect(callback, [source])接收两个参数,调用方式如下

useEffect(() => {
   console.log('mounted');
   
   return () => {
       console.log('willUnmount');
   }
 }, [source]);
复制代码

** 生命周期函数的调用主要是通过第二个参数[source]来进行控制,有如下几种情况:

  • [source]参数不传时,则每次都会优先调用上次保存的函数中返回的那个函数,然后再调用外部那个函数;
  • [source]参数传[]时,则外部的函数只会在初始化时调用一次,返回的那个函数也只会最终在组件卸载时调用一次;
  • [source]参数有值时,则只会监听到数组中的值发生变化后才优先调用返回的那个函数,再调用外部的函数。

connect 原理

connect 实际上就是一个 HOC,在原应用组件上包裹一层,使原来整个应用成为Provider的子组件 接收Reduxstore作为props,通过context对象传递给子孙组件上的connect

接收 mapStateToPropsmapDispatchToProps,然后返回一个生产Component的函数(wrapWithConnect),然后再将真正的Component作为参数传入wrapWithConnect,这样就生产出一个经过包裹的Connect组件,

Fiber?

官方的解释是:React Fiber是对核心算法的一次重新实现 ** 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。而关键点,便是 同步阻塞。在之前的调度算法中,React 需要实例化每个类组件,生成一颗组件树,使用 同步递归 的方式进行遍历渲染,而这个过程最大的问题就是无法 暂停和恢复。

  • React V16 将调度算法进行了重构, 将之前的 stack reconciler 重构成新版的 fiber reconciler,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启
  • 这里我理解为是一种 任务分割调度算法,主要是 将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改