[面试] - React常见面试题

506 阅读13分钟

前言

记录一些个人认为比较常见的React相关的常见面试题以及个人理解答案,如有错误请见谅以及指出,谢谢

1.1、React生命周期

class Test extends React.Component{
    constructor(props){ // 构造函数
        super(props)
        this.state = {}
    }
    // 初始化,更新时会调用
    static getDerivedStateFromProps(props,state){
        // 必须返回一个对象,会和state合并
        return {}
    }
    // 初始化渲染时使用
    componentDidMount(){}
    // 组件更新时调用 返回false 不更新
    shoudComponentUpdate(prevProps,nextState){ return true }
    // 组件更新时调用,返回的值会设置在componentDidUpdate的第三个参数
    getSnapshotBeforeUpdate(prevprops,nextState) { return ''}
    // 组件更新后调用
    componentDidUpdate(preProps,preState,valueFromSnaspshot){}
    // 组件卸载时调用
    componentWillUnmount() {}
    // 组件抛出错误
    static getDerivedStateFromError(){}
}

b8256be3fb51644e5c0e11489db35381_1500x797.webp

1.2、JSX

JSX:JSX 本质是javascript的语法拓展,它和模板语言相似,但是具备javascript的能力,直白的说,JSX 是React.createElement()的语法糖

  • JSX会编译成React.createElement(),React.createElement将返回一个ReactElement的js对象,编译工作交由babel操作
<div className="testDiv"><h1 className="testH1">content</h1></div>
// 上面jsx代码会转译成下面内容 React.createElement(标签名,标签参数,标签内容)
React.createElement('div',{ className: 'testDiv'},
   React.createElement('h1', { className: 'testH1' }, 'content')
)

1.3、类组件和函数组件

  • 在开发的时候,类组件是基于面向对象编程的,主打继承、生命周期等核心概念,而函数组件的内核是函数式编程,主打immutable/引用透明等

  • 性能优化上,类组件主要依靠shouldComponentUpdate来阻断渲染来提升性能,而函数组件依靠React.memo缓存渲染结果来提高性能

  • 从趋势上,React官方更推崇 组合优于继承 的设计概念,所以未来函数组件成为主推方案的概率会大些

1.4、react组件设计模式

无状态组件(展示组件)

只作展示、独立运行、不额外增加功能的组件,特点是复用性强,可分为代理组件样式组件布局组件

有状态组件(灵巧组件)

包含业务逻辑和数据状态的组件称为有状态组件,或者灵巧组件,特点是功能丰富、复杂度高、复用性低,有状态组件分为 容器组件高阶组件

  • 容器组件 - 几乎没有复用性,主要用于拉取数据和组合组件
  • 高阶组件 - 实际是一个函数概念,一个函数可以接收另一个函数作为参数,然后返回一个函数,称为高阶函数

1.5、高阶组件

  • 实际是一个函数概念,一个函数可以接收另一个函数作为参数,然后返回一个函数,称为高阶函数
// 高阶组件 判断登录状态
const checkLogin = (WrappedCompont) => {
    return props => {
        const isLogin = true; // 这里是登录判断条件,根据实际处理,未登录则显示登录组件
        return isLogin ? <WrappedCompont {...props} /> : <LoginPage />
    }
}
// 调用高阶组件,函数写法
class Demo extends React.Component{}
const DemoPage = checkLogin(Demo)
// 装饰器写法
@checkLogin
class Demo extends React.Component{}

1.6、setState的同步异步

  • setState是先存入state队列还是直接更新,根据isBatchingUpdates值判断,true则存入队列(异步),否则直接更新(同步)
  • 在react可控制的事件里,是执行异步,如react的生命周期事件和合成事件(如onClick)
  • 在react无法控制的事件,则执行同步,如原生事件、addEventListener、setTimeout等

1.7、调用setState后会发生什么

  • 调用setState后,react会将传入的参数对象和组件当前的状态合并,然后触发调和过程返回新的状态,react会根据新的状态构建新的DOM树,然后重新渲染

1.8、组件通信

单层级通信

  • 父组件给子组件通信,传递props
  • 子组件向父组件通信,使用回调函数
  • 兄弟组件通信,通过父组件当中间件传递

跨多层组件通信

  • React的context api
  • 使用全局变量和事件,绑定在window
  • 使用状态管理框架,如 redux
  • 自定义事件订阅模式组件(不推荐,不好管理)

1.9、虚拟DOM的工作原理,Virtual DOM 也称VDOM,也就是虚拟DOM

虚拟DOM的工作原理是通过js对象模拟DOM的节点
以react为例,在render函数中写的jsx会在babel插件下,编译为React.createElement执行的JSX中的属性参数,
React.createElement执行后会返回一个描述节点的对象,描述自己的tag类型,props属性以及children 子级等,
这些对象通过树形结构组成一个虚拟DOM树,在状态发生改变时,对比更新前后虚拟DOM树的差异,这个过程称为diff,生成的结果称为patch,计算之后,会渲染patch完成对真实DOM的操作

虚拟DOM优点

  • 改善大规模DOM操作的性能
  • 规避XSS风险
  • 以较低成本实现跨平台开发

虚拟DOM缺点

  • 内存占用较高(因为需要模拟整个网页的真实DOM)
  • 高性能应用场景存在难以优化的情况

1.10、diff算法

diff算法是指生成更新补丁的方式,主要运用于虚拟DOM树变化后,更新真实DOM

react的diff算法,触发更新的时间主要是在state变化和hooks调用之后。此时触发虚拟DOM树变更遍历,采用了深度优先的遍历算法,采用3种类型节点的比对,分别是树、组件和元素

  • 树比对 - 由于网页视图中较少出现跨层级节点移动,所以两个虚拟DOM树只对同一层级的节点进行比较
  • 组件比对 - 如果组件是同一类型,则进行树对比,如果不是则直接放入补丁中
  • 元素比对 - 主要发生在同层级中,通过节点标记(key)操作生成补丁,节点操作对应真实DOM的操作

React16起,引入了Fiber架构,是为了使整个更新过程可以随时暂停和恢复,节点和树分别采用了FiberNode和FiberTree进行重构,FiberNode使用了双链表的结构,可以直接找到兄弟节点和子节点

1.11、react渲染流程

  • react16以后的渲染流程是Fiber Reconciler

Fiber Reconciler特点

  • 协作性多任务模式 - 线程会定时放弃自己当前运行权利,交给主线程运行
  • 策略性优先级 - 调度任务通过标记tag的方式区分优先级执行

渲染过程基本可以划分为 Render 和 Commit 两个阶段

  • Render 阶段主要是计算出diff树,自下而上逐个节点检查并构造新的树,特点是可终止
  • Commit 阶段是根据diff更新DOM树,回调生命周期等,同步执行,不可中断暂停

1.12、key的作用

  • key是react用于追踪哪些列表的元素被更改的辅助标识

在开发过程中,需要保证某个元素的key在同级元素中是唯一的

在react diff算法中react会借助元素的key值来判断该元素是新创建的还是被移动而来的元素,从而减少不必要的元素重渲染

1.13、createElement和cloneElement的区别

  • React.createElement和React.cloneElement都是用来创建react元素的,区别是传参不一样
  • createElement传入的第一个参数是react元素,而cloneElement第一个参数是element

1.14、受控组件和不受控组件

  • 每当表单状态发生变化时,都会被写入组件的state中,这种组件成为受控组件
  • 受控组件:没有维持自己的状态,状态由父组件控制,仅通过父组件的props获取当前值,然后通过回调函数通知更改
  • 不受控组件:保持着自己的状态,refs用于获取当前值

1.15、context 多层级通信

context的作用是为了避免在组件间层层传递变量,我们可以通过createContext(null)来创建一个新的context,新创建的context包含一个provider 和一个consumer

传递时,需要用Provider包裹父组件,在Provider包裹下的层层组件中,通过consumer包裹子组件来读取穿度的变量

1.16、组件状态保存(类似vue的keep-alive)

  • vue的keep-alive是把虚拟DOM 保存在内存中,React认为容易造成内存泄漏,所以官方不提供状态保存方案

1、手动保存状态

配合componentWillUnmount生命周期,通过redux之类的状态管理框架对数据进行保存,通过componentDidMount进行数据恢复,但数据量大的时候比较麻烦

2、通过路由实现保存 react-router

这个方法实现比较麻烦,原理是因为react状态丢失是由于路由切换卸载了组件引起的,所以从根本上改变路由对组件的渲染行为,有以下方式

  • 重写组件 - 可参考 react-live-route,实现成本比较高
  • 重写路由库 - 可参考 react-keeper, 实现成本和风险更高

3、模拟真实功能

github 有类似的实现插件 react-activation,实现原理是:由于react 会卸载掉处于固有组件层级内的组件,所以我们需要将children子属性抽取处理,渲染到一个不会被卸载的组件内,再使用DOM操作将其真实内容移到对应的内容,实现此功能

1.17、useEffect和useLayoutEffect

相同点

  • useEffect和useLayoutEffect都是用于处理副作用,也就是改变DOM、设置订阅、操作定时器等
  • 使用方法一样,底层也一样,都是调用mountEffectlmpl方法,基本可以直接替换

不同点

  • useEffect在React渲染过程中是被异步调用的,用于绝大部分场景,而useLayoutEffect会在所有DOM变更后同步调用,主要处理DOM操作、调整样式、避免页面闪烁灯问题,因为是同步,所以在较大计算量时会造成阻塞
  • useLayoutEffect总是会比useEffect先执行

1.18、react-hooks使用限制

主要有两个限制

  • 不要在循环、条件、嵌套函数中调用hook
  • 在react的函数组件中调用hook
    因为hooks的设计是基于数组实现,在调用时是按顺序加入数组中,如果使用循环、条件等有可能会导致数组取值错位,导致执行到错误的hook

1.19、immutable

  • immutable 是指不可改变的数据,创建后不会更改,任何修改都会返回一个新的immutable对象

  • 使用immutable可以给react应用带来性能的优化,主要体现在减少渲染次数

  • 在react性能优化的时候,为了避免重复渲染,通常是在shouleComponentUpdate中做对比,判断返回是否需要返回true更新

  • immutable通过is方法可以完成对比,无需通过深度比较的方式比较

1.20、React性能优化方法

  • 1、减少渲染的节点、降低渲染的计算量(复杂度)
  • 2、避免重复渲染,类组件通过shouldComponentUpdate来比对state和props(浅比较),确认是否需要重新渲染,函数组件则是用React.memo
    • shouldComponentUpdate:React.PureComponent类内置了对shouldComponentUpdate的实现,如果要深对比,可以加入Immutable.js
    • memo和useMemo:两个都是函数组件的优化思路
    • React.memo是函数版的shouldComponentUpdate,但只针对props,不针对state的变化
    • useMemo则是更加精细的memo,可以控制是否需要重复执行某一段逻辑

1.21、react懒加载

  • react16.6 新增了React.lazy函数,配合webpack的codesplitting可以实现懒加载,只有组件加载时才会导入对应资源
// 不用lazy
import otherComponent from './otherComponent'
// 使用lazy
const otherComponent = React.lazy(()=>import('./otherComponent'))

1.22、useEffect如何区分生命周期

  • useEffect可以看成是 componentDidMount、componentDidUpdate和componentWillUnmount三者的结合
useEffect(()=>{
    console.log('mounted')
    return ()=>{ console.log('willunmount') }
},[source])

// source不传则是默认componentDidMount,传参后就是对应参数的componentDidUpdate

1.23、常见的hook

  • useState - 状态钩子,定义组件的state
  • useEffect - 生命周期钩子
  • useContext - 获取context对象
  • useCallback - 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染
  • useMemo - 缓存传入的props,避免重复渲染
  • useRef - 获取组件的真实节点

1.24、不同版本做过什么优化

React15: 架构可以分为两层

  • reconciler(协调器) - 负责找出变化的组件
  • render(渲染器) - 负责把变化的组件渲染到页面上

在React15及以前,reconciler(协调器)采用递归的方式创建虚拟DOM,递归过程是不可中断的,如果层级很深,递归时间超过了16ms,用户交互就会卡顿

因此,React将递归改成异步可中断的Filber

  • React16: 架构可分为三层
  • scheduler(调度器) - 负责调度任务的优先级,优先级高的任务先进入reconciler
  • reconciler(协调器) - 负责找出变化的组件
  • render(渲染器) - 负责把变化的组件渲染到页面上

1.25、如何理解react

  • React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质上是一个组件化框架

  • 它的设计核心思路有三点: 声明式,组件化和通用性

  • 声明式的优势在于直观和组合

  • 组件化的优势在于视图的拆分与模块复用,可以更容易做到高内聚低耦合

  • 通用性在于一次学习,随处编写,比如react native等,这里主要靠虚拟DOM来保证实现

  • React的劣势在于它没有提供完整的解决方案,需要开发者自行整合,比如react-router等

1.26、副作用

  • 纯函数

  • 确定的输入,产生确定的输出,与执行次数、时间无关

  • 不产生副作用

  • 常见副作用

  • 系统IO相关API

  • Date.now()、Math.random()等不确定性方法

  • 在函数体内修改函数外变量的值

  • 在函数体内修改函数参数的值

  • 调用会产生副作用的函数

  • http请求

2.1、redux是什么

redux是一个实现状态集中管理的容器,遵循三大基本原则

  • 单一数据源
  • state是只读的
  • 使用纯函数来执行修改
    store的数据,通过dispatch来派发action

redux工作流程

view 调用store的dispatch接收action传入store,reducer进行state操作,然后view通过store提供的getState获取最新的数据

2.2、常用中间件

  • redux-thunk - 用于异步操作
  • redux-promise - 处理异步操作
  • redux-sage - 处理异步操作
  • redux-logger - 用于日志记录

2.3、react-redux

  • react-redux是官方推荐的库,具有高效且灵活的特性

  • react-redux分成两个核心provider 和 connection

// Provider
<Provider store = {store}> <App /> </Provider>

// connection
import { connect } from 'react-redux'
// 把redux的数据映射到react的props中去
const mapStateToProps = state => {
    return {foo: state.foo}
}
// 将redux中的dispatch映射到组件内部的props中去
const mapDispatchToProps = dispatch =>{
    return {
        toclick: () => { dispatch({type:'toclick'}) }
    }
}
connect(mapStateToProps, mapDispatchToProps)(MyComponent)

2.4、reducer

  • reducer是纯函数,它规定应用程序的状态怎样因响应action而变化,reducer通过接收先前的状态和action来工作, 然后返回一个新的状态。
  • 它根据操作的类型确定需要执行哪种更新,然后返回新的值,如果不需要完成任务,就会返回原来的状态