【前端面试】React相关

React 整体渲染流程

为什么要有React Fiber?

见:React Fiber的意义

jsx、ReactElement、虚拟DOM 、Fiber的关系

树里的一个 React 元素(ReactElement)对应一个 Fiber,一个 Fiber 也对应树里的一个元素。
见:JSX、ReactElement、FiberNode、DomElement

常见的生命周期函数都是在哪些渲染阶段被调用

见:常见的生命周期函数都是在哪些渲染阶段被调用

diff算法的过程

单节点和多节点Diff:Diff算法

fiberRootNode与rootFiber的结构

fiberRootNode 是“整棵应用的根管理对象(Root 容器)”,而 rootFiber 是“Fiber 树里的第一个 Fiber 节点(HostRootFiber)”。

见:FiberRootNode存储的信息Fiber Node存储的信息

setState后React都做了什么

见:从一次 useState 的 setState 开始后发生了什么

React Scheduler为什么一定要把任务包在宏任务里执行?

思考能否使用如下方案:直接同步执行,限制好执行时间(5ms)
答案是,不行。

Scheduler本质上做的不仅仅是分片,还要把控制权交还给浏览器

  • 纯同步执行 + 自己看时间,理论上可以“分段”,但本质上你还是在当前调用栈里,浏览器拿不到控制权,不能及时处理输入/渲染。
  • React 需要的是协作式调度:做一小段 -> 让出线程 -> 下轮再继续。这就必须依赖异步边界(MessageChannel/setTimeout 这种 task 机制)。

React Scheduler 为什么不用requestIdleCallback实现

  1. 兼容性差 Safari 并不支持 https://caniuse.com
  2. 控制精细度 React 要根据组件优先级、更新的紧急程度等信息,更精确地安排渲染的工作
  3. 执行时机requestIdleCallback(callback) 回调函数的执行间隔是 50ms(W3C规定),也就是 20FPS,1秒内执行20次,间隔较长。
  4. 差异性 每个浏览器实现该API的方式不同,导致执行时机有差异有的快有的慢
requestIdleCallback的替代方案?

MessageChannel

选择 MessageChannel 的原因,是首先异步得是个宏任务,因为宏任务中会在下次事件循环中执行,不会阻塞当前页面的更新。MessageChannel 是一个宏任务。

没选常见的 setTimeout,是因为MessageChannel较快执行,在 0~1ms 内触发,像 setTimeout 即便设置 timeout 为 0 还是需要4~5ms。相同时间下,MessageChannel 能够完成更多的任务。

并发渲染

支持 new concurrent renderer(并发模式的渲染)

  • 并发模式不是一个功能,而是一个底层设计。
  • 它可以帮助应用保持响应,根据用户的设备性能和网速进行调整。
  • 通过渲染可中断来修复阻塞渲染机制。在 concurrent 模式中,React 可以同时更新多个状态。
  • 区别就是使同步不可中断更新变成了异步可中断更新。
  • useDeferredValue 和 startTransition 用来标记一次非紧急更新。

starTransition:用于标记非紧急的更新,用 starTransition 包裹起来就是告诉 React,这部分代码渲染的优先级不高,可以优先处理其它更重要的渲染。

useTransition:除了能提供 startTransition 以外,还能提供一个变量来跟踪当前渲染的执行状态。

React 的并发机制允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。

并发机制的工作原理

时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。

优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。

可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。

并发机制的优势
  • 提升响应性: 通过优先处理高优先级任务,React 能够更快地响应用户输入,提升用户体验。
  • 优化性能: 将渲染任务拆分为小片段,避免长时间的渲染阻塞,提升应用的整体性能。
  • 更好的资源利用: 在主线程空闲时处理低优先级任务,充分利用系统资源。
如何启用并发模式

要在 React 应用中启用并发模式,需要使用 createRoot API:

1
2
3
4
5
6
7
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

在并发模式下,React 会自动根据任务的优先级进行调度和渲染。

useTransition 本质:把一部分更新标记到“过渡 lane(低优先级车道)”,让紧急更新(输入、点击)先渲染,过渡更新可以被打断、延后。

useDeferredValue 本质:把某个值的更新“映射”到过渡 lane,上游状态先变,依赖这个值的重渲染滞后一点,起到“值级别的 useTransition”效果。

什么是lane模型

见:Lane模型

状态更新与批量更新机制

同一个上下文中的setState会被合并

React 17以及之前:

  • onClick 里的多个 setState 会被批处理
  • setTimeout / Promise.then 里的更新通常不会自动批处理(会各自触发)

React 18(createRoot)

引入了更广泛的自动批处理:

  • React 事件里批处理
  • setTimeout、Promise、原生事件回调里也会自动批处理(多数场景)

setState 是同步还是异步的

useState怎么实现的第一次拿初始状态,后续拿之前状态

判断挂载还是Update,如果是Update阶段根据BaseState和Update对象计算出最新的state

useState等hooks为什么不能写在函数组件外面

因为hook要挂到当前Fiber上

React18 的自动批处理与 flushSync

  • 在 react17 中,只有 React 事件会进行批处理,原生 js 事件、promise,setTimeout、setInterval 不会。
  • react18 中,将所有事件都进行批处理,即多次 setState 会被合并为 1 次执行,提高了性能。

flushSync:退出批量更新,强制立即刷新视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React,{useState} from "react"
import {flushSync} from "react-dom"

const App=()=>{
const [count,setCount]=useState(0)
const [count2,setCount2]=useState(0)

return (
<div className="App">
<button onClick=(()=>{
// 第一次更新
flushSync(()=>{
setCount(count=>count+1)
})
// 第二次更新
flushSync(()=>{
setCount2(count2=>count2+1)
})
})>点击</button>
<span>count:{count}</span>
<span>count2:{count2}</span>
</div>
)
}
export default App

其他 React18/19 新特性

  • 放弃对 IE 浏览器的支持:react18/19 引入的新特性全部基于现代浏览器,如需支持需要退回到 react17 版本。
  • Suspense 不再需要 fallback 捕获。
  • 支持 useId:在服务器和客户端生成相同的唯一一个 id,避免 hydrating 的不兼容。
  • useSyncExternalStore:用于解决外部数据撕裂问题。
  • useInsertionEffect:这个 hooks 只建议在 css in js 库中使用,这个 hooks 执行时机在 DOM 生成之后,useLayoutEffect 执行之前,无法访问 DOM 节点引用,一般用于提前注入脚本。

React Hooks

见:Hooks

封装自定义 Hooks

实现 useTimeout hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// callback 回调函数, delay 延迟时间
function useTimeout(callback, delay) {
const memorizeCallback = useRef();

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

useEffect(() => {
if (delay !== null) {
const timer = setTimeout(() => {
memorizeCallback.current();
}, delay);
return () => {
clearTimeout(timer);
};
}
}, [delay]);
};

实现一个 dom 可见性的 hook

实现的一个关键点是 Ref 的更新在 useEffect 执行前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useEffect, useState, useRef } from "react";
const useInView = (
options = {
root: null,
rootMargin: "0px 0px",
threshold: 1,
},
triggerOnce = false, // 是否只触发一次
) => {
const [inView, setInView] = useState(false);
const targetRef = useRef(null);

useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setInView(true);
if (triggerOnce) {
// 触发一次后结束监听
observer.unobserve(entry.target);
}
} else {
setInView(false);
}
});
}, options);

if (targetRef.current) {
// 开始监听
observer.observe(targetRef.current);
}

return () => {
if (targetRef.current) {
// 组件卸载时结束监听
observer.unobserve(targetRef.current);
}
};
}, [options, triggerOnce]);

return [targetRef, inView];
};

export default useInView;

状态管理相关

React Hooks 如何实现类似 Redux 的状态管理

useContext + useReducer。

Redux 性能问题

Redux vs Mobx

Redux RTX

ImmerJS

React Router 路由模式与实现原理

几种模式

<BrowserRouter> /<HashRouter>

  • hash 模式(<HashRouter>):在 url 后面加上 #,如 http://127.0.0.1:5500/home/#/page1
  • history 模式(<BrowserRouter>):允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

hash 模式

URL 示例:https://example.com/#/about、https://example.com/#/users/123

history模式

URL 示例:https://example.com/about、https://example.com/users/123

history模式最重要的是部署需要配合Nginx,所有路由都返回 index.html