2021-02-19 React面试题总结-持续更新

422 阅读20分钟

1、React中的key是什么,有什么作用

key是react在渲染一系列相同类型的兄弟元素时,给每个元素指定一个稳定可预测兄弟间唯一的值,来帮助React识别哪些元素改变了,比如添加和删除,这样做可以避免在某些场景下的错误渲染并且提升React的渲染性能

key的作用是用在使用diff算法对比react更新前后两棵树的比较时使用的,使得树的转换效率得以提高,组件实例基于它们的key来决定是否更新以及复用;另外一个作用是当做使用map数据结构存储fiber节点的key值,以便于取节点时的方面

2、refs是什么,如何使用,需要注意什么

refs是提供一种方式让我们访问DOM节点或在render方法中创建React元素,在某些特殊的情况下,能够获取到组件实例或者DOM元素进行操作

创建refs的三种方式

第一种:React.createRef(),并通过ref属性附加到React元素上,访问时,用ref的current属性,React会在组件挂载时给current属性传入DOM元素或子组件实例,并在组件卸载时传入null值,ref会在componentDidMount和componentDidUpdate生命周期钩子触发前更新

注意:ref的值根据节点的类型不同而有所不同

  • 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性
  • 当ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性
  • 不能在函数组件上使用ref属性,因为他们没有实例

第二种,由于函数组件没有实例,如果要在函数组件中使用ref,可以用forwardRef转发,React.forwardRef(props,ref)函数接收ref(ref不是prop属性,就像key一样)作为第二个参数将其向下传递给子组件,另外用hooks中的useRef()方法在函数组件中创建ref

第三种,用callback方式参数是(ele)=>{this.refs = ele},不建议使用内联函数的方式,冗余

3、React最新的生命周期是怎样的

React16之后有三个生命周期被废弃(但并未被删除)分别是:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

目前React17+的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段

挂载阶段

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

更新阶段

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

卸载阶段

  • componentWillUnmount

上述简介的都是类组件,如果是函数组件使用Hooks则如下:

  • 组件挂载
  • 执行副作用
  • 组件更新
  • 执行清理函数
  • 执行副作用
  • 组件准备卸载
  • 执行清理函数
  • 组件卸载

4、React组件通信如何实现

React组件间通信方式:

  • 父组件向子组件通信:父组件可以向子组件通过传递props的方式,向子组件进行通信
  • 子组件向父组件通信:props+回调的方式,父组件向子组件传递props进行通信,此props作为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息作为参数,传递到父组件中
  • 兄弟组件间通信:找到这两个兄弟节点共同的父节点,结合之前两种方式由父节点转发信息进行通信
  • 跨层级通信:利用高级API-context上下文的方式,context设计的目的是为了共享那些对于一个组件树而言是全局的数据。这里直接转到我的另外一篇博客里面有详细介绍React高级API-Context
  • 发布订阅模式:发布者发布事件,订阅者监听事件并作出反应,可以通过引入Event模块进行通信
  • 全局状态管理工具:借助redux等全局状态管理工具进行通信,这种工具会维护一个全局状态中心store,并根据不同的事件产生新的状态

5、React合成事件是什么

  • React中有自己的事件系统模式,通常被称为React合成事件。
  • 之所以采用自己定义的合成事件,一方面为了抹平事件在不同平台提现出来的差异性,使得开发者不需要自己再去关注浏览器事件兼容问题;另一方面是为了统一管理事件,提高性能,这主要体现在React内部实现了事件委托,并且记录当前事件发生的状态
  • 事件委托,即事件代理,这种机制不会把事件处理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听函数和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件放生时,首先被这个统一的事件监听器处理,然后在映射表里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大的提升
  • 记录当前事件发生的状态,即记录事件执行的上下文,这便于React来处理不同事件优先级,达到谁优先级高处理谁的目的,这里也就实现了React的增量渲染思想,可以预防掉帧,同时达到页面更加顺滑的目的,提升用户体验

6、细读setState

setState(updater, [callback])

其将对组件的state的更改排入队列,并通知React需要使用更新后的state重新渲此组件及其子组件。这是用于更新用户界面以及响应事件处理器和处理服务器数据的主要方式

React会延迟调用setState,然后通过一次传递更新多个组件,不会保证state的变更立即生效,如果要立即拿到this.state的值,可以在其回调函数callback或者componentDidUpdate里取到最新的值

参数updater有两种形式:

第一种是函数形式

this.setState((state, props) => stateChange)

第二种是对象形式,会进行批量更新,如果要使用前一次的state值,请使用函数形式

this.setState({value:2})

setState中的异步与同步

异步是指批量更新,达到性能优化的目的,在合成事件和生命周期中都是异步的,不能立马拿到this.state的值

同步是指立马能拿到最新的this.state值,在定时器setTimeout和原生事件中是同步的,另外在其第二个参数回调函数callback中和componentDidUpdate更新生命周期函数中也能得到最新的this.state值

7、比较state与props

state是一种数据结构,在当前组件有效,使用setState进行更改,class组件中会引发组件渲染

函数组件中可以用useState、useReducer,与class组件不同的是,如果前后两次的值相同,函数组件中的useState和useReducer Hook会放弃更新,原地修改setState不会引起重新渲染

通常,不应该在React中修改本地state。然而作为一条出路,可以用一个增长的计数器来在state没变的时候依然强制一次重新渲染

const [ignored, forceUpdate] = useReducer(x=>x+1, 0)

function handClick(){
	forceUpdate();
}

这也是函数组件中类似class组件中的this.forceUpdate()函数

而props是组件的属性值,由父组件传递过来,就组件自身来说,props是不可变的,组件不能改变自己的props,但是可以把子组件的props放在一起统一管理,props可以是多种数据类型,如对象、函数等

props和class的state都是普通的Javascript对象。它们都是用来保存信息的,这些信息可以控制组件的渲染输出,而它们的一个重要的不同点就是:props是传递给组件的(类似于函数的形参),而state是在组件内被组件自己管理的(类似于在一个函数内声明的变量)

propsstate
是否能从父组件中拿到初始值
是否可以被父组件改变
在组件内是否可以设置默认值
在组件内是否可以改变
是否可以为子组件设置初始值
在子组件中能改变吗

8、函数组件和类组件的区别

  • 语法上,函数使用简单就一个props参数,而类组件要实现继承,还要有render方法,使用起来要先实例化
  • 状态管理,函数复用状态逻辑很简单,抽取出来放在自定义Hooks中即可,类组件则要使用render props或者HOC,较麻烦
  • 生命周期,函数组件使用一个useEffect就可以实现,而类组件则要实现ComponentDidMount等多个生命周期钩子函数
  • 函数式组件总是捕获了渲染所使用的值,因为props总是不变的,而this总是改变的

9、React性能优化方案

1、减少不必要渲染,如用shouldComponentUpdate、PureComponent、React.memo实现

2、数据缓存

- useMemo缓存参数、useCallback缓存函数
- 函数、对象尽量不要使用内联形式(如context的value object、refs function)
- Route中的内联函数渲染时候使用render或者children,不要使用component,当你用component的时候,Router会用指定的组件和React.createElement创建一个新的[React elment]。这意味着当你提供的是一个内联函数的时候,每次创建render都会创建一个新的组件。这会导致不再更新已经现有组件,⽽是直接卸载然后再去挂载⼀个新的组件。因此,当⽤到内联函数的内联渲染时,请使⽤render或者children

3、不要滥用功能项,如context、props等

10、React中组件之间共享状态逻辑的三种方式

注意:不是指组件之间相互通信的方式

React中的组件共享状态逻辑和组件相互通信的区别: 共享状态逻辑就像免费公园,人人都可以进入;而相互通信则像是去医院看病一样,必须交钱得有条件后才能获得治疗

前者比如redux、react-redux,后者比如涉及到props和context传递数据等

React中有三种方式共享状态逻辑:

  1. render prop

     render prop是指一种在React组件之间使用一个值为函数的prop共享代码的简单技术,具有render prop的组件接收一个返回React元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑
     
    
    <DateProvider render={data=>(
        <h1>Hello {data.target}</h1>
    )} />
    
     使用render prop的组件库有常见的React-Router等
     要记住render prop是因为模式才被成为render prop,你不一定要用名为render的prop来使用这种模式。事实上,任何用于被告知组件需要渲染什么内容的函数prop在技术上都可以被称为'render prop',例如我们可以使用children prop!
     
    
    <Mouse children={mouse=>(
        <p>鼠标的位置是{mouse.x}, {mouse.y}</p>
    )} />
    
     记住,children prop并不真正的需要添加到JSX元素的'attributes'列表中。相反,可以直接放置到元素的内部
    
    <Mouse>
        {mouse=>(
            <p>鼠标的位置是{mouse.x}, {mouse.y}</p>
        )}
    </Mouse>
    
  2. 高阶组件(HOC)

     高阶组件HOC是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式
     具体而言,高阶组件是参数为组件,返回值为新组件的函数
    

    const EnhancedComponent = higherOrderComponent(WrappedComponent)

     组件是将props转换为UI,而高阶组件是将组件转换为另一个组件
     HOC在React的第三方库中很常见,例如redux的connect、react-router中的withRouter等
     注意1:不要再render中使用HOC
     注意2:refs不会被传递
    
  3. 自定义hooks

     自定义Hook是React16.8的新增特性,可以在不编写class的情况下使用state以及其他React特性
     通过自定义Hook,可以将组件逻辑提取到课重用的函数中
     自定义Hook是一个函数,其名称以'use'开头,函数内部可以掉用其他的Hook,如下例子:
     
    
    import {useState, useEffect} from 'react'
    
    function useFriendStatus(friendID){
        const [isOnline, setIsOnline] = useState(null);
        
        useEffect(()=>{
            function handleStatus(status){
                setIsOnline(status.isOnline);
            }
            
            ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
            return ()=>{
                ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
            }
        });
        
        return isOnline;
    }
    
     注意事项:
     1、自定义Hook必须以'use'开头
     2、两个组件中使用相同的Hook不会共享state,所有的state和副作用都是完全隔离和相互独立的,是因为我们调用自定义Hook相当于只是调用了useState和useEffect等,而React Hook是可以使用多次的,所以它们是完全独立的
    

11、React Fiber到底是什么

  1. 为什么需要fiber

    对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用,结果就是主线程上的布局、动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体验

  2. 任务分解-就是解决上面的问题

  3. 增量渲染-把渲染任务拆分成块,匀到多帧

  4. 更新时能够暂停、终止、复用渲染任务

  5. 给不同类型的更新赋予了优先级

  6. 并发方面新的基础能力

  7. 更流畅

fiber的数据结构其实是链表,是指组件上将要完成或者已经完成的任务,每个组件可以有一个或多个,如下图:

fiber.png

12、调用setState之后发生了什么

  • 在代码中调用了setState函数之后,React会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)
  • 经过调和过程,React会以相对高效的方式根据新的状态构建React元素树并且着手重新渲染整个UI界面
  • 在React得到元素树之后,React会自动计算出新树与老树的节点差异,然后根据差异对界面进行最小化重渲染
  • 在差异计算算法中,React能够相对精确的知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染

13、react-router里的Link标签和a标签有什么区别

Link是react-router里实现路由跳转的链接,配合Route使用,react-router拦截了其默认的链接跳转行为,区别于传统的页面链接,只会触发相应匹配的Route对应的页面内容更新,不会刷新整个页面

a标签是html的原生超链接,用于跳转到href指向另一个页面或者锚点元素,跳转新页面会刷新页面

14、react-hooks的优劣如何

React Hooks优点

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

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

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

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

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

React Hooks的缺陷

  • 只能在最顶层出现,不能出现在条件、循环和嵌套中,而且只适用React函数或者自定义Hooks中,不能定义在普通的Javascript函数里
  • 破坏了PureComponent(只适用于类组件)、React.memo(只适用于函数组件)浅比较的性能优化效果(为了取最新的props和state,每次render都要重新创建事件处理函数)
  • 在闭包场景可能会引用到旧的state、props值
  • 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
  • React.memo并不能完全替代shouldComponentUpdate(因为拿不到state change,只针对props change)

15、你的技术栈主要是React,那你说说使用React有什么坑点

  1. JSX做表达式判断的时候,需要强制转为boolean类型,如果不使用!!b进行强制数据类型转换,会在页面输出0
render() {
    const b=0;
    return <div>
        { !!b && 这是一段文本 }  //  页面输出为空!!b为false,!b页面输出文字
    </div>
}
  1. 尽量不要在componentWillReceiveProps里使用setState,如果一定要使用,那么需要判断结束条件,不然会出现无限重渲染,导致页面崩溃(实际不是componentWillReceiveProps会无限重渲染,而是componentDidUpdate)

  2. 给组件添加ref时候,尽量不要使用匿名函数,因为当组件更新的时候,匿名函数会被当做新的props处理,让ref属性接收到新函数的时候,react内部会先清空ref,也就是会以null为回调参数先执行一次ref这个props,然后在以该组件的实例执行一次ref,所以用匿名函数做ref时候,有的时候取ref赋值后的属性会得到null

  3. 遍历子节点的时候,不要用index作为组件的key进行传入

16、怎么去设计一个组件封装

  • 组件封装的目的是为了重用,提高开发效率和代码质量
  • 低耦合、单一职责、可复用性、可维护性

17、React的虚拟dom是怎么实现的

  • 首先说说为什么要使用Virtual DOM,因为操作真实DOM的耗费性能代价太高,所以React内部使用js实现了一套dom结构,在每次操作真实dom之前,使用实现好的diff算法,对虚拟dom进行比较,递归找出有变化的dom节点,然后对其进行更新操作
  • 为了实现虚拟dom,需要把每一种节点类型抽象成对象,每一种节点类型都有自己的属性,也就是prop,每次进行diff的时候,react会先比较该节点类型,如果节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中
  • 如果节点类型一样,那么会比较prop是否有更新,有变化则react会判定该节点有更新,重新渲染该节点,然后再对其子节点进行比较,一层一层往下,直到没有子节点

18、React Hooks原理是什么

Hooks使用闭包实现的,因为纯函数不能记住状态,只能用通过闭包来实现

19、useState中的状态时怎么存储的

通过单向链表,fiber tree就是一个单项链表的树形结构

20、说一下redux的三大原则

  1. 单一数据源
  2. state是只读的,更改状态的唯一方法是dispatch一个action
  3. 使用reducer纯函数来执行修改

21、render props、HOC和Hooks的看法

  • 三者都可以对组件状态逻辑进行复用
  • render props将一个返回React元素的函数作为props属性来实现自己的渲染逻辑,函数可以接受内部的state作为参数,如react-router等库有实现,但是容易形成嵌套地狱
  • HOC创建一个函数接收组件作为参数,返回一个不同的组件,不会影响内层组件的状态,降低了耦合度,但是props可能会有重叠被覆盖,无法得知数据来源函数组件还是类组件
  • Hooks就是一个函数,可以重命名,会标记数据来源,简单易懂非常直观,不会嵌套

22、redux的工作流程

首先了解下redux的几个核心概念:

  • store:一个数据容器,用来管理和保存整个项目的state。在整个应用中只能有一个store
  • state:一个对象,在state中存储相应的数据,当开发者需要使用数据时,则可以通过store提供的方法getState来获取
  • action:一个通知命令,用于对state进行修改。通过store提供的方法dispatch可以发起action完成对state的修改
  • dispatch:发起action的唯一方法
  • reducer:纯函数,用来对state的修改

纯函数是指:

1) 该函数的执行过程中无任何副作用。如:网络请求、DOM操作、定时器等

2) 如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖其输入参数

基本流程如下

  • 基于reducer创建store
  • 从store中获取state传递给视图
  • 当视图被操作时,通过dispatch发起一个action
  • store接收action之后,会把state和action传递给reducer
  • reducer更新state并把新的state传递给视图进行更新

23、react-redux是如何工作的

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

  • connect:负责连接React和Redux

      - 获取stateconnect通过context获取Provider中的store,通过store.getState()方法获取store tree上所有state
      - 包装原组件:将state和action通过props的方式传入到原组件内部,再以高阶组件的方式把connect中的mapStateToProps和mapDispatchToProps与原来组件的props合并,在返回一个新组件
      - 监听store tree变化:connect中使用到了redux中subscribe方法监听了组件状态的变化
    

24、你是如何理解React Fiber的

概括的讲:React Fiber可以理解为React内部实现的一套状态更新机制。支持任务的不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。包含以下三层含义:

  1. 作为架构来说,之前的React15的协调Reconciler采用递归的方式执行,数据保存在递归调用栈中,称为stack Reconciler。而React16的协调Reconciler基于Fiber节点实现,称为Fiber Reconciler
  2. 作为静态数据结构来说,每个Fiber节点对应一个React Element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息
  3. 作为动态单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)

25、用Hooks实现类似于this.setState({a:1}, cb)

使用自定义Hooks方式:useStateHooks()

返回[value, setValue],但是setVaule有两个参数(arg,callback)

function useStateHooks() {
  const [value, setValue] = useState(null);

  const ref = useRef(null);

  const fn = useCallback(
    (arg, callback) => {
      setValue(arg);
      ref.current = callback;
    },
    [arg]
  );

  useEffect(() => {
    ref.current();
  }, [ref.current]);

  return [value, fn];
}

26、React Hooks中关于useEffect的事件循环面试题

以下代码log输出几次?说明原因

import { useEffect, useState } from "react";

function Count() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(1);
  });

  console.log("1");

  return (
    <div>
      <p>{count} times</p>
    </div>
  );
}

export default Count;

解答:log会输出3次:第一次是正常的普通函数执行;第二次是执行useEffect里的setCount,这时候由于setCount返回值是1与当前值0不一样,会重渲染函数;第三次是执行第二次更新的useEffect,此时setCount返回值1与当前的值还是不一样,会再触发重渲染,后面由于值都一样,所以不会在渲染了

这里要注意:useEffect的回调函数是异步宏任务,会在下一轮事件循环中执行即渲染线程之后,而useLayoutEffct和componentDidMount、componentDidUpdate是异步微任务,会在本轮事件循环后,渲染线程前执行

持续更新...