2024 react 面试题学习笔记

374 阅读19分钟

JS发展路径:模版字符串->数据驱动

react vue 对比

相同点:

1 都是 MVVM 前端开发的架构模式

2 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件

3 使用 Virtual DOM 更新视图,react 是环形链表,Vue 是 rectivity VDOM

4 都支持跨平台渲染

5 只专注于核心库的开发和维护,将路由、全局状态管理、UI 组件、打包编译的脚手架等交给其他的库

不同点:

1 开发方式:react是JSX 更加灵活、Vue是模版化Templates,相对上手简单
2 生态:react生态更加丰富(国外),Vue也发展很快(国内)

3 都是单向数据流,但是Vue能够双向数据绑定

4 diff流程:react是从根节点向下调和(判断不可变数据),Vue是数据代理proxy(可变数据),能够直接更新组件

5 设计思想:react 偏原生(Runtime),Vue 偏抽象(权衡) (angular/serverlet 强编译时,很多api接口)

createElement

react.createElement 接受3个参数:type,[props],[...children]

组件本质上就是类和函数,承载UI和更新视图的setState、useState等方法

React在底层逻辑上处理的组件:实例化类和执行函数

对于class组件中,除了继承React.Component ,类组件执行构造函数过程中会在实例上绑定props和context,初始化置空refs属性,原型链上绑定update对象等

jsx代码通过react.createElement转换成react element对象后,最终在调和阶段,element对象的会形成与之对应的fiber对象,然后通过sibling、return、child形成单向循环链表关联起来

react/src/ReactBaseClasses.js

function Component(props, context, updater) {
  this.props = props;      //绑定props *****
  this.context = context;  //绑定context
  this.refs = emptyObject; //绑定ref
  this.updater = updater || ReactNoopUpdateQueue; //上面所属的 updater 对象
}
/* 绑定setState 方法 */
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
}
/* 绑定forceupdate 方法 */
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
}

对于函数组件的执行,是在react-reconciler/src/ReactFiberHooks.js

function renderWithHooks(
  current,          // 当前函数组件对应的 `fiber`, 初始化
  workInProgress,   // 当前正在工作的 fiber 对象
  Component,        // 我们函数组件
  props,            // 函数组件第一个参数 props
  secondArg,        // 函数组件其他参数
  nextRenderExpirationTime, //下次渲染过期时间
){
     /* 执行我们的函数组件,得到 return 返回的 React.element对象 */
     let children = Component(props, secondArg);
}

image.png

组件间的通信方式

  1. 父子组件:props 和 callback 方式
  2. 跨层级组件:context上下文方式、ref 方式、event bus事件总线
  3. 全局组件:react-redux状态管理方式

受控组件 非受控组件

组件里的value受到props控制,用onChange回调函数处理value的改变并同步到state中,这样的组件就是受控组件。缺点是如果有很多个value,就需要写很多回调函数控制,代码比较臃肿。

非受控组件即value不与state关联,需要用到的时候,用ref直接去DOM中获取

setState 的执行流程*******

1 首先创建一个update对象和更新的优先级 expirationTime (新lane),放入当前的更新队列 enqueueUpdate 中

2 然后开始调度更新,从fiberRoot根节点向下调和子节点,调和阶段找到发生更新的组件,合并state,然后执行render函数,得到新的UI视图层,完成 render 阶段。

3 接着在commit阶段,替换真实的DOM,完成页面视图更新

4 最后检查setState中是否有callback函数需要执行,有就执行,没有就结束更新流程

image.png

setState 批量更新

react事件执行之前, 通过 isBatchingEventUpdates=true 打开开关,然后在scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新

在promise、setTimeout异步事件中,可以跳出批量更新

不过也可以用 unstable_batchedUpdates,在异步函数中,也能批量更新

在实际工作中,unstable_batchedUpdates 可以用于多次Ajax数据交互之后,合并多次 setState,或者是多次useState

image.png

setState useState

相同点: setState和useState底层都是调用了scheduleUpdateOnFiber方法,都是批量更新

不同点:

1 useState会默认浅比较两次state是否相同,然后决定是否更新组件

2 setState有专门监听state变化的callback,可以获取最新state,
但在函数组件中,改变的state只有在下一次函数组件执行时才能拿到,可以通过useEffect来监听state变化引起的副作用

3 setState是新老state进行合并使用,而useState是重新创建再赋值

props 可以是什么

① props 作为一个子组件渲染数据源

② props 作为一个通知父组件的回调函数

③ props 作为一个单纯的组件传递

④ props 作为渲染函数

⑤ render props,和④的区别是放在了children属性上。

⑥ render component插槽组件

在标签内部的属性和方法会直接绑定在props对象的属性上,对于组件的插槽会被绑定在props的chidren属性中

生命周期

初始化流程

image.png

更新流程

image.png

卸载

image.png

getDerivedStateFromProps (nextProps, prevState)

在初始化和更新流程中,接受父组件的props数据,可以对props进行格式化,过滤等操作,返回值将作为新的state合并到state中

静态属性方法,内部是访问不到 this 的,它更趋向于纯函数。

getDerivedStateFromProps 作用

  • 取代 componentWillMount 和 componentWillReceiveProps
  • 组件初始化或者更新时,将 props 映射到 state 中
  • 返回值与state合并完,newState可以作为shouldComponentUpdate第二个参数

UNSAFE_xxx

componentWillMount、componentWillReceiveProps、componentWillUpdate这三个生命周期,都是在render之前执行的,滥用这几个生命周期,可能导致生命周期内的上下文多次被执行,甚至可能发生死循环

ref

用createRef、useRef可以创建出一个ref原始对象,可以把一些没必要更新视图的数据储存到ref对象中,只要组件没有销毁,ref对象就一直存在,这样能够直接修改数据,不会触发重新渲染。

useEffect,useMemo引用useRef对象中的数据,无须将ref对象添加成dep依赖项,因为ref指向一个内存空间,可以 随时访问到变化后的值

ref 挂载位置

类组件有一个实例 instance 能够维护像 ref 的各种信息
函数组件 useRef 产生的ref对象挂到函数组件对应的 fiber 上 (没有实例对象)

ref 执行时机

时机:对Ref的处理,都是在commit阶段发生的。 commit阶段会进行真正的Dom操作,此时ref就是用来获取真实的DOM以及组件实例的。

逻辑:DOM更新之前 commitDetachRef, DOM 更新之后 commitAttachRef

image.png

Suspense 异步组件

原理: 调用 Render => 发现异步请求 => 渲染终止、捕获异常、等待异步请求完毕 => 拿到数据、再次渲染

优点: 拿到数据后再渲染 (少渲染一次)

image.png

错误边界

componentDidCatch() 会在commit阶段被调用,允许 再次触发 setState,来降级UI渲染,

static getDerivedStateFromError 是静态方法,内部不能调用 setState。它的返回的值可以合并到 state,作为渲染使用。

虚拟列表

原理: 虚拟列表是一种长列表的解决方案, 在长列表滚动过程中,只有视图区域显示的是真实 DOM ,滚动过程中,不断截取视图的有效区域,让人视觉上感觉列表是在滚动。达到无限滚动的效果。

实现思路:

1 通过 useRef 获取元素,缓存变量

2 useEffect 初始化计算容器的高度。截取初始化列表长度。这里需要div占位,撑起滚动条。

3 通过监听滚动容器的onScroll事件,根据scrollTop来计算渲染区域向上偏移量

4 最后重新计算 end 和 start 高度来重新渲染可视区域+缓冲区域的列表

虚拟列表划分可以分为三个区域:视图区 + 缓冲区 + 虚拟区。

合成事件的好处:

• 为了抹平不同浏览器的差异,提供合成事件对象

• 减少内存消耗,提升性能,(不需要注册那么多的事件了,一种事件类型只在 Root上注册一次)

• 对事件进行归类,可以在事件产生的任务上包含不同的优先级

合成事件流程:

1 首先会把原方法,通过 react事件插件映射表,转成react事件

2 在渲染过程中,fiber向上收集事件依赖,形成事件列表,放在fiber的memorizeState中,再向container容器注册事件监听器(document-v17+container),并bind fiber对象

3 最后dispatchEvent触发事件执行队列,模拟捕获、执行、冒泡的过程

// onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']

Hooks

Hooks 为什么出现

1 让函数组件有状态,处理副作用,能获取ref,也能做数据缓存

2 解决逻辑复用难的问题

3 放弃面向对象编程,拥抱函数式编程

Hooks 保存状态

函数组件的每个hooks都对应着一个workInProgressHook对象,hooks信息保存在对应fiber的memoizedState中,每个hooks通过next链表关联,形成一个Effect副作用链表

image.png

useState dispatchAction 原理

首先:每次调用dispatchAction都会先创建一个update,然后把它放入待更新pending队列中

然后:判断如果当前的fiber是否正在更新,如果是,则停止更新流程

最后:如果fiber没有更新任务,则继续更新,会浅比较 state,如果相同,那么直接退出更新;如果不相同,那么发起更新调度任务。


当再次执行useState的时候,会触发更新hooks逻辑,本质上调用的就是 updateReducer,会把待更新的队列 pendingQueue 拿出来,合并到 baseQueue,循环进行更新。

useMemo useCallback 的区别

useMemo 需要执行第一个函数,缓存执行的结果。(只计算一次)

useCallback 缓存第一个参数,针对可能重新创建的函数

useEffect和useLayoutEffect异同

useEffect 是异步执行 ,对于每一个effect的callback,React会向 setTimeout 回调函数一样,放入任务队列,等到主线程任务完成,DOM更新后,才执行
所以useEffect回调函数不会阻塞浏览器绘制视图

useLayoutEffect是在DOM绘制之前,同步执行,会阻塞浏览器绘制。

可以方便修改DOM ,修改后浏览器只会绘制一次


总结:修改DOM,改变布局就用useLayoutEffect,其他情况都用 useEffect

对于函数组件fiber,updateQueue存放每个useEffect/useLayoutEffect产生的副作用组成的链表。在commit阶段更新这些副作用

FC 函数组件 CC 类组件

相同点:

1 是组件的两种类型,两者底层都依赖于fiber架构实现渲染,只是前置的vdom计算过程有区别

不同点:FC CC

1 state状态:

函数本身无状态,不能调用setstate,需要对应的hook来实现

类组件默认绑定了update对象,有自己的状态

2 生命周期:

函数组件没有生命周期,也需要配合useEffect来实现

类组件有完整的生命周期

3 上手难度:

函数组件代码相对轻量级,但是对应的各种hook就需要理解才能正确使用,上手快,升入难

类组件是oop开发方式,有this、state、render、生命周期等概念需要理解,上手慢、使用简单

4 逻辑复用:

函数组件好复用,抽离公共逻辑,写成hook

类组件复用逻辑需要高阶组件HOC来实现

5 更新:

函数组件每次更新都会重新创建变量

类组件只需要实例化一次,后续更新都尽量复用实例上的状态、节点

6 心智模型:

函数组件是一种偏声明式的'数据到视图'的映射

类组件是一种偏命令式的面向对象实践,注重数据与行为的分装

diff 算法*******

调用 reconcileChildrenArray 来调和子代 fiber,进行新老fiber的对比。

1 同层遍历, 同层节点 sibling 指针移动遍历,判断fiber节点上的tag和key是否匹配,没有变化的节点直接复用oldFiber

2 当分层遍历结束时,统一删除没有复用的 oldfiber

3 当oldFiberMap为null,如果还有新的children,统一创建对应的 newFiber

4 当新旧节点都还有剩余,针对 发生移动 的情况, mapRemainingChildren 数组中查找有没有可以复用的 oldFiber

5 最后删除剩余没有复用的 oldFiber

diff O(n^3) 优化到 O(n)

传统diff算法:通过循环递归对节点进行依次对比,再对比插入删除节点,算法复杂度O(n^3)

新的diff算法的3个优化策略:

tree diff:分层遍历,忽略跨层级移动DOM节点,只会遍历一次

component diff:判断组件类型 tag 是否一样,不一样则直接删除老组件、创建新组件

element diff:对于同一层级的子节点,用唯一的key值进行判断

调度 时间片

异步调度-时间片

GUI渲染线程和JS线程是相互排斥的。react的更新是交给浏览器自己控制的,react从根节点开始diff更新时,会通过类似requestIdleCallback去向浏览器做一帧一帧的请求,如果浏览器有绘制任务就先绘制,在空闲时再执行diff更新任务,这样来减少页面卡顿,提升体验

消息通道:目前requestIdleCallback只有谷歌浏览器支持
为了兼容每个浏览器:

IE浏览器 -> setImmidiate

主要用messageChannel高频短间的去请求时间切片 (类似于宏任务的机制)

兜底 -> setTimeout

image.png

异步调度流程*******

image.png

fiber 调和

fiber 是什么

  1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为Stack Reconciler栈调和, 循环递归遍历VDOM,可能会造成页面卡顿;在新的架构中,Reconciler(协调器)是基于fiber实现的,节点数据保存在fiber中,所以被称为 fiber Reconciler。
  2. 作为静态数据结构来说,fiber是 React element对象和真实DOM之间的桥梁,每个fiber就是一个节点,保存了这个节点的基本信息,这个时候,fiber节点就是虚拟DOM。
  3. 作为动态工作单元来说,fiber节点保存了该节点需要更新的状态,以及需要执行的副作用,fiber在React中就是最小粒度的执行单元。

React Fiber是一种新的协调引擎,它具有改进协调算法、支持增量渲染、可中断和恢复、支持并发模式、向后兼容和支持错误边界等特点,从而提高了React应用的性能、可扩展性和健壮性。

image.png

fiber 的属性

function FiberNode(){

  this.tag = tag;                  // fiber 标签 证明是什么类型fiber。
  this.key = key;                  // key调和子节点时候用到。 
  this.type = null;                // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。  
  this.stateNode = null;           // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
 
  this.return = null;              // 指向父级fiber
  this.child = null;               // 指向子级fiber
  this.sibling = null;             // 指向兄弟fiber 
  this.index = 0;                  // 索引

  this.ref = null;                 // ref指向,ref函数,或者ref对象。

  this.pendingProps = pendingProps;// 在一次更新中,代表element创建
  this.memoizedProps = null;       // 记录上一次更新完毕后的props
  this.updateQueue = null;         // 类组件存放setState更新队列,函数组件存放
  this.memoizedState = null;       // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
  this.dependencies = null;        // context或是时间的依赖项

  this.mode = mode;                //描述fiber树的模式,比如 ConcurrentMode 模式

  this.effectTag = NoEffect;       // effect标签,用于收集effectList
  this.nextEffect = null;          // 指向下一个effect
  this.firstEffect = null;         // 第一个effect
  this.lastEffect = null;          // 最后一个effect

  this.expirationTime = NoWork;    // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。

  this.alternate = null;           // 双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}

调和流程 **********

1 首先创建fiberRoot根节点和rootFiber组件节点

2 构建出current和workInProgress双缓存fiber树,用alternate关联起来

3 通过beginwork 向下调和 reconcileChildren子节点 ,通过diff算法复用或者增删节点

通过completeUnitOfWork 向上归并,如果有兄弟节点,会返回sibling兄弟,没有则return父级fiber,一直返回到fiebrRoot根节点

4 最后会以workInProgress作为最新的渲染树,替换成current树

image.png

调和流程图

image.png

中断和恢复的贪心思想:每次有时间都会去尝试执行更新

1 如果能执行完,就 commit 提交指令 更新 DOM

2 没有就继续执行 while 循环

3 有更高优先级的任务就放弃已经执行了的 task

渲染

shallowEqual 浅比较流程***

1 先判断新老props、state 是否是对象

2 接着比较新老props、state 的对象是否相等

3 然后判断对象的属性长度是否相等 (Object.keys)

4 最后再逐个遍历比较 key 是否相等

React 性能优化方案

1 渲染前:减少提交的更新

react.lazy+suspense异步加载组件

useRef挂载不用更新的数据(非受控组件)

避免使用内联对象、匿名函数(每次渲染执行都会产生新的props对象,导致子组件重复渲染)

状态下放,缩小状态影响的范围

列表渲染使用唯一的 key 值

2 渲染中:跳过不必要的更新

FC:React.memo+useCallback、useMemo 缓存组件
CC:shouldComponentUpdate、pureComponent 函数判断是否需要重新渲染

3 常见的前端的通用优化

分页按需加载组件、防抖、节流、虚拟列表

V18 新增了哪些特性

1 createRoot renderer api: Concurrent Mode(并发模式的渲染) 可以同时更新多个状态(从(V17)同步不可中断更新变成了(V18)异步可中断更新)

2 setState 自动批处理(多次更新合并成一次)

3 flushSync 提高state更新的优先级

4 React 组件的返回值可以为undefined、null

5 严格模式下,第二次渲染在控制台的日志将显示为灰色

6 Suspense不再需要fallback来捕获 (展示null)

7 新的Hook:useId(组件ID)、useSyncExternalStore(状态)、useInsertionEffect(注入css)

image.png

并发模式的理解

1 并发更新的意义就是交替执行不同的任务,当预留的时间不够用时,React将线程控制权交还给浏览器,等待下一帧时间到来,然后恢复中断的工作

2 并发模式是实现并发更新的基本前提

3 时间切片是实现并发更新的具体手段

4 都是基于 fiber 架构实现的,fiber为状态更新提供了可中断的能力

Lane expirationTime 优先级

LanePriority:

fiber优先级: 位于react-reconciler包的 Lane 车道模型

1:Lane类型被定义为二进制变量, 利用了位掩码的特性, 在频繁计算的时候占用内存少, 计算速度快

2:Lane和Lanes就是单数和复数的关系, 代表单个任务的定义为Lane, 代表多个任务的定义为Lanes

共定义了18 种车道(Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为Lane和Lanes类型

占有低位比特位的Lane变量对应的优先级越高

最高优先级为SyncLanePriority对应的车道为SyncLane = 0b0000000000000000000000000000001

最低优先级为OffscreenLanePriority对应的车道为OffscreenLane = 0b1000000000000000000000000000000.对应为离屏渲染

3:Lane 是对于 expirationTime 的重构, react 17 里以前使用 expirationTime 表示的字段, 都改为了 lane

使用 Lanes 模型相比 expirationTime 模型的优势

1 Lanes把任务优先级从批量任务中分离出来, 可以更方便的判断单个任务与批量任务的优先级是否重叠

2 Lanes 使用单个 32 位二进制变量即可代表多个不同的任务, 也就是说一个变量即可代表一个组 (group), 要在一个 group 中分离出单个 task, 非常容易

SchedulerPriority

调度优先级

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

ReactPriorityLevel

优先级等级,负责上面2套优先级体系的转换

react-router

history React-router React-router-dom

image.png

模式与核心对象:

/history模式:
www.xxx.com/home

window.history.pushState

/#/hash模式:
www.xxx.com/#/home

window.location.hash

redux mobx

redux 的工作流程

1 createStore 生成数据中心 Store

2 action定义行为

3 dispatch发起action

4 reducer处理action,返回新的state

image.png

1 单向数据流

2 state只读,要改变state需要触发action来执行reducer

3 reducer是纯函数

middleware 中间件机制

Redux middleware是一种可插拔的机制,用于在dispatch函数被调用后, reducer处理action之前,对action进行拦截、变换、增强等操作

redux-thunk redux-saga

原生的dispatch是不支持异步的,要使用redux-thunk或者redux-saga等中间件才能支持

redux-thunk:

优点:功能简单、体积小、上手快 (20+行)

缺点:

样板代码过多,而且很多是重复的

异步操作与redux的action偶合在⼀起,不⽅便管理

redux-saga:

优点:

异步解耦: 异步操作被转移到单独saga.js中

异常处理: 代码异常/请求失败 都可以直接通过try/catch语法直接捕获处理

功能强⼤: redux-saga提供了⼤量的Saga辅助函数和Effect创建器供开发者使⽤

缺点:

有一定的上手成本(10+API)

体积庞⼤(2千行代码)

mobx 原理

Mobx 采用了一种'观察者模式'——Observer;

使用流程:

1 初始化绑定状态 observable 、激活状态 makeObservable

2 依赖收集: 通过 @observable 包裹

3 派发更新

Redux Mobx 区别

1 Redux是单向数据流
Mobx依赖于Proxy、Object.defineProperty等,去劫持get 、set ,数据变化多样性

2 Redux可拓展性比较强,可以通过中间件自定义增强 dispatch

3 在Redux中,基本只有一个store ,统一管理store下的状态
在mobx中,可以有多个模块,每一个模块都有一个独立的 store

4 首先Mobx更容易上手。比如Redux想使用异步,需要配合中间件,流程比较复杂