
Q1:Fiber 架构的工作原理?
核心要点:Fiber 是 React 16 引入的协调引擎(Reconciliation Engine),将渲染工作拆分成可中断、可恢复的最小工作单元。
工作原理:
- 数据结构:每个 Fiber 节点对应一个 React 元素,包含
type、key、child、sibling、return、pendingProps、memoizedProps、memoizedState、alternate(双缓冲)等字段,形成一棵 Fiber 树。 - 双缓冲机制:维护两棵 Fiber 树(
current和workInProgress),更新时构建workInProgress,完成后整体切换,减少页面闪烁。 两阶段渲染:
- Render 阶段(可中断):调用
beginWork向下遍历,构建新 Fiber 树,标记副作用(Placement/Update/Deletion)。 - Commit 阶段(不可中断):将副作用一次性提交到真实 DOM,调用
useEffect/useLayoutEffect回调。
- Render 阶段(可中断):调用
- 时间切片(Time Slicing):通过
MessageChannel或scheduler包将渲染任务切成 5ms 小片,每帧requestIdleCallback思想交还主线程。
简化流程图:
scheduleUpdate → scheduleWork → workLoop (yield when no time)
→ beginWork (down) → completeWork (up) → commitWork (sync)Q2:ReactReconciler 为何要采用 Fiber 架构?
原因总结:
- 解决同步阻塞问题:React 15 之前采用 Stack Reconciler,递归调用栈一旦开始无法中断,长任务(>16ms)会阻塞主线程,导致卡顿、掉帧。
- 支持优先级调度:Fiber 节点可携带
lanes(优先级位),实现高优先级更新(用户输入)打断低优先级更新(数据加载)。 - 支持并发特性:为 Concurrent Mode、
useTransition、useDeferredValue等 React 18 特性提供底层支撑。 - 更好的错误恢复:可中断后从断点恢复,结合 Error Boundary 提升健壮性。
Q3:useState 是如何实现的?
实现原理(简化源码):
// 挂载阶段
function mountState(initialState) {
const hook = mountWorkInProgressHook(); // 创建 hook 节点挂到 fiber.memoizedState 链表
hook.memoizedState = typeof initialState === 'function' ? initialState() : initialState;
hook.queue = { pending: null, dispatch: null };
const dispatch = dispatchAction.bind(null, currentlyRenderingFiber, hook.queue);
hook.queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
// 更新阶段
function dispatchAction(fiber, queue, action) {
const update = { action, next: null, lane: requestUpdateLane() };
// 链表追加 update
enqueueRenderPhaseUpdate(queue, update);
// 调度更新
scheduleUpdateOnFiber(fiber, update.lane);
}关键点:
- Hook 以链表形式存储在 Fiber 节点的
memoizedState上; dispatch通过调度器触发更新;- 多次
setState在同一事件中会批处理(React 18 自动批处理,Promise/setTimeout 中也合并)。
Q4:React Fiber 是什么?
定义:Fiber 是 React 内部的协调算法 + 数据结构,是对"虚拟 DOM 节点"的升级版。
两个层面的含义:
- 架构层面:新的协调引擎(Reconciler)。
- 数据结构层面:每个 Fiber 节点是一个 JS 对象,包含组件信息、DOM 信息、工作状态、副作用链表等。
核心属性:
| 属性 | 说明 |
|---|---|
type / elementType | 组件类型('div' / FunctionComponent / ClassComponent) |
key | Diff 时用于识别节点 |
stateNode | 真实 DOM / 类实例 |
return / child / sibling | 树结构指针 |
pendingProps / memoizedProps | 新旧 props |
memoizedState | Hook 链表 / 类组件 state |
flags / subtreeFlags | 副作用标记(Placement/Update/ChildDeletion) |
lanes / childLanes | 优先级 |
alternate | 指向另一棵树的对应节点(双缓冲) |
Q5:简单介绍 React 中的 diff 算法
三大策略(将 O(n³) 降为 O(n)):
- Tree Diff(树级):同层比较,不跨层级移动节点。若发现节点跨层移动,直接删除重建。
Component Diff(组件级):
- 同类型组件:继续往下 diff;
- 不同类型组件:直接替换整棵子树。
Element Diff(元素级):通过
key标识同级元素的唯一性。- 有
key:用 key 匹配节点,移动位置而非重建; - 无
key:按顺序逐个对比,效率低且易出错。
- 有
最佳实践:列表渲染务必提供稳定且唯一的 key(建议用业务 ID,不要用 index)。
Q6:如何让 useEffect 支持 async/await?
直接写法(错误):
useEffect(async () => { // ❌ 返回 Promise,React 不会等待
await fetchData();
}, []);正确写法:
// 方案1:内部定义 async 函数
useEffect(() => {
const load = async () => {
const data = await fetchData();
setData(data);
};
load();
}, []);
// 方案2:抽成函数
const fetchData = async () => { /* ... */ };
useEffect(() => { fetchData(); }, []);原因:useEffect 的回调要么返回 undefined,要么返回清理函数(cleanup)。async 函数返回 Promise,会被当作"清理函数"忽略,导致清理逻辑失效。
可选优化:使用 AbortController 取消请求:
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal }).then(...);
return () => ctrl.abort();
}, [url]);Q7:React Fiber 是如何实现更新过程可控?
可控性的三大支柱:
- 可中断:每次执行一个 Fiber 单元后检查
shouldYield(),时间片用完就return,把控制权交回调度器。 - 可恢复:通过
workInProgress记住当前进度,下次从断点 Fiber 继续beginWork。 - 可优先级:每个更新携带
lane(优先级),高优先级插入队列时可插队抢占低优先级。
实现关键:
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function shouldYield() {
return performance.now() >= deadline; // 5ms 时间片
}调度器基于 MessageChannel 实现,比 setTimeout(fn, 0) 更稳定,比 requestIdleCallback 兼容性更好。
Q8:React 中懒加载的实现原理是什么?
核心 API:React.lazy + Suspense。
原理:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<OtherComponent />
</Suspense>
);
}React.lazy接收返回 Promise的动态 import();- 首次渲染时 React 抛出 Promise,进入 Suspense 的 fallback;
- Promise resolve 后重新渲染,替换为真实组件;
- 底层通过 Throw Promise 模式让渲染可中断,等模块加载完成再恢复。
路由级懒加载(React Router v6):
const Home = lazy(() => import('./pages/Home'));
<Route path="/" element={<Suspense fallback={<Loading />}><Home /></Suspense>} />Q9:React 中怎么实现状态自动保存(KeepAlive)?
场景:列表页 → 详情页 → 返回时保留列表滚动位置和筛选条件。
方案对比:
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| 缓存组件状态 | 外部 Store(Zustand/Redux)保存状态 | 中小型项目 |
| 手动缓存 | useRef + useImperativeHandle 暴露 reset | 单组件 |
| 第三方库 | react-activation / react-keep-alive-router | 完整方案 |
| DOM 节点缓存 | 隐藏的 display: none 容器保留 DOM | 简单场景 |
react-activation 示例:
import { KeepAlive } from 'react-activation';
<KeepAlive name="list-page" saveScrollPosition>
<ListPage />
</KeepAlive>底层通过 Portal 将组件挂载到隐藏容器,路由切换时不卸载。
Q10:React 有哪些性能优化的方法?
按层级:
1. 减少渲染次数
React.memo/PureComponent浅比较 props;useMemo/useCallback缓存引用;- 状态下推(拆分组件,缩小影响范围);
- Context 拆分(细粒度 Context)。
2. 减少渲染计算量
- 虚拟列表(
react-window、react-virtuoso); - 防抖/节流高频事件;
- Web Worker 处理 CPU 密集任务。
3. 减少包体积
- 路由懒加载 + 组件懒加载;
- Tree Shaking;
- 按需引入第三方库;
- 压缩图片(WebP/AVIF)。
4. 网络优化
- CDN、HTTP 缓存、Service Worker;
- 接口合并、GraphQL;
- 预加载(
<link rel="preload">)。
5. 渲染层
- SSR/SSG(Next.js);
- Suspense 流式渲染。
Q11:不同版本的 React 都做过哪些优化?
| 版本 | 关键优化 |
|---|---|
| React 15 | Stack Reconciler,diff 算法奠基 |
| React 16 | Fiber 架构、Error Boundaries、Fragment、Portal、createRef |
| React 16.3 | new Context API、React.createRef、React.forwardRef |
| React 16.6 | React.lazy + Suspense、memo、forwardRef |
| React 16.8 | Hooks 革命(useState/useEffect/useContext 等) |
| React 17 | 渐进式升级、新 JSX 运行时(react/jsx-runtime)、事件委托到根容器 |
| React 18 | Concurrent Renderer、自动批处理、Transitions(useTransition / useDeferredValue)、Suspense SSR 流式、useId、useSyncExternalStore |
| React 19(实验) | Actions、useOptimistic、use() API、ref 作为 prop、<form> Action、<title> 文档元数据 |
Q12:React 18 新特性
Concurrent Features(并发渲染)
createRootAPI 启用并发模式;useTransition:标记非紧急更新;useDeferredValue:延迟更新值。
- 自动批处理:所有更新(包括 Promise、setTimeout、原生事件处理器)合并渲染。
Suspense 改进:
- 服务端 Suspense 流式 HTML(Streaming SSR);
<SuspenseList>协调多个 Suspense。
- 新 Hook:
useId、useSyncExternalStore、useInsertionEffect。 - 严格模式增强:开发模式下组件二次挂载/卸载,复现副作用问题。
- Server Components(RSC):服务端组件,零客户端 JS。
Q13:React Hook 的闭包陷阱是什么?解决方案?
陷阱示例:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远打印 0,闭包陷阱
setCount(count + 1); // 永远 +1(基于 0)
}, 1000);
return () => clearInterval(id);
}, []); // 空依赖,count 永远是最初的 0
}解决方案:
函数式更新(推荐):
setCount(c => c + 1); // 总是基于最新值使用 ref 保存最新值:
const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]); // 定时器内使用 countRef.current- 正确声明依赖:
useEffect(..., [count]),但可能造成定时器反复创建/销毁。
根因:每次渲染都会创建一个新的 effect 函数,effect 捕获的是创建时的 props/state。
Q14:React 中,怎么给 children 添加额外的属性?
方案 1:React.cloneElement(最常见)
function MyContainer({ children }) {
return React.Children.map(children, child =>
React.cloneElement(child, { extraProp: 'value' })
);
}方案 2:Context 透传(更优雅)
const ExtraCtx = createContext({});
function Parent({ children }) {
const extra = { theme: 'dark' };
return <ExtraCtx.Provider value={extra}>{children}</ExtraCtx.Provider>;
}方案 3:HOC / 自定义 Hook 包装
方案 4:React 19 直接 ref 作为 prop,未来属性传递将更自然。
Q15:Fiber 为什么是 React 性能的一个飞跃?
飞跃点:
- 从同步到异步:Stack Reconciler 必须一次性跑完,Fiber 可中断,让出主线程给高优先级任务。
- 从单优先级到多优先级:基于
lane模型实现 5+ 种优先级,用户输入、动画、数据加载可差异化调度。 - 从一次性提交到分阶段:Render/Commit 分离,DOM 变更可控,避免半成品状态。
- 为并发特性铺路:
useTransition、Suspense、Server Components全部依赖 Fiber。 - 降低主线程压力:长列表、大状态树更新不再卡顿,60fps 成为可能。
Q16:React 是否支持给标签设置自定义的属性?
支持,但不推荐。
两种方式:
- HTML 自定义属性:原生支持,
data-*推荐使用,可通过dataset访问。 非标准属性:React 会原样传递到 DOM 上(除了
key/ref/dangerouslySetInnerHTML等特殊属性)。<div custom-attr="hello" data-id="123" />
注意事项:
- 会触发 React 警告
Unknown prop "custom-attr" on <a> tag(如需消除,加// @ts-ignore或扩展 JSX 类型); - 推荐优先用
data-*,符合 HTML5 规范且语义清晰; - 复杂场景建议用 Context 或全局状态管理。
Q17:说说 React render 阶段的执行过程
核心流程:
- 入口:
scheduleUpdateOnFiber→ 调度器根据lane决定立即/延后执行; 构建 workInProgress 树:
- 复用
current节点(通过alternate); - 调用
beginWork(current, workInProgress, renderLanes)处理当前 Fiber;
- 复用
beginWork 内部:
- 函数组件:执行函数体,收集 Hook;
- 类组件:调用
render方法; - 调和子节点,标记 Diff(flags);
- completeWork:向上回溯,完成 Fiber 节点信息收集,构建 effect 链表;
- 循环:在
workLoopConcurrent中通过shouldYield()检查时间片,用完则暂停。
特点:纯 JS 计算,不操作 DOM,可随时中断。
Q18:React 中,Fiber 是如何实现时间切片的?
实现机制:
- 最小工作单元:单个 Fiber 节点(
performUnitOfWork); 时间片控制:
- 调度器设置
deadline = performance.now() + 5ms; - 每完成一个 Fiber 调用
shouldYield()检查是否超时; - 超时则退出 workLoop,保留
workInProgress指针;
- 调度器设置
- 恢复机制:浏览器空闲后(通过
MessageChannel.postMessage)重新进入 workLoop,从上次断点继续; - 底层调度器:
scheduler包实现了基于优先级队列和最小堆的调度算法。
为什么不直接用 requestIdleCallback:
- 兼容性差(仅 Chrome 支持);
- 最小延迟 50ms 太长;
- 无法精细控制优先级。
Q19:说说 React commit 阶段的执行过程
Commit 阶段三个子阶段:
BeforeMutation(DOM 变更前)
- 调用类组件
getSnapshotBeforeUpdate; - 调度
useEffect销毁函数。
- 调用类组件
Mutation(DOM 变更)
- 执行所有 DOM 插入/更新/删除;
- 绑定/解绑 ref;
- 调用类组件
componentDidMount/componentDidUpdate。
Layout(DOM 变更后、浏览器绘制前)
- 调用
useLayoutEffect回调; - 调度
useEffect创建函数(异步,避免阻塞绘制)。
- 调用
关键特性:
- 同步执行,不可中断(保证 DOM 一致性);
- 整个过程通过
commitRoot一次性提交 effect 链表。
Q20:React 中的路由懒加载是什么?原理?
概念:按路由拆分代码块,访问对应路由时才加载对应组件的 JS。
实现:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}原理:
import()触发 webpack/vite 拆包(自动生成单独 chunk);React.lazy把模块包装成懒加载组件;- 首次渲染时 React 抛 Promise,进入 Suspense fallback;
- 模块加载完成 resolve 后重新渲染。
优势:首屏 JS 体积显著减少,加快 FCP/TTI。
Q21:React 中为什么不直接使用 requestIdleCallback?
原因:
- 兼容性问题:仅 Chrome/Firefox 支持,Safari/Edge 早期不支持;
- 最大延迟 50ms:对于需要快速响应的场景(动画、输入),延迟过高;
- 无法精细优先级:
requestIdleCallback只区分 idle 与否,无法满足多优先级调度; - 无法可靠触发:浏览器在标签页非激活时可能完全停止调用,影响数据一致性。
React 的替代方案:
- 使用
MessageChannel宏任务; scheduler包实现自定义优先级队列;- 通过
shouldYield()+performance.now()控制时间片。
Q22:React 组件间怎么进行通信?
通信方式汇总:
| 场景 | 方式 |
|---|---|
| 父子 | props / 回调函数 |
| 子父 | 回调函数 / ref 暴露实例 |
| 兄弟 | 状态提升到共同父组件 / Context |
| 跨层级 | Context API / Redux / Zustand |
| 任意组件 | 全局状态(Redux / MobX / Zustand / Jotai) |
| 深层组件 | Composition(children / render props) |
| 命令式 | useImperativeHandle + forwardRef |
| 外部系统 | useSyncExternalStore 订阅外部 Store |
最佳实践:
- 优先用 props / composition;
- 跨多层级才考虑 Context;
- 复杂应用用 Redux Toolkit / Zustand;
- 避免过度使用 Context(导致不必要的 re-render)。
Q23:React 和 Vue 在技术层面有哪些区别?
| 维度 | React | Vue 3 |
|---|---|---|
| 范式 | 函数式 + JSX,强调"UI = f(state)" | 选项式 + 组合式,渐进式框架 |
| 响应式 | 不可变数据 + 显式 setState | Proxy 代理 + 自动依赖收集 |
| Diff | 单端比对 | 双端对比 + 最长递增子序列 |
| 编译优化 | React Compiler(实验) | 模板编译 + 静态提升 + 补丁标志 |
| Fiber/调度 | 必备 | 不需要(响应式粒度更细) |
| 生态 | 庞大、社区驱动 | 官方全家桶(Router/Pinia/Vite) |
| 学习曲线 | 较陡(Hooks、心智模型) | 较平缓(模板直观) |
| TypeScript | 一流支持 | 良好支持(3.x 改进) |
| 包体积 | 较小核心(~44KB gzip) | 较小核心(~34KB gzip) |
| 服务端 | Server Components / Next.js | Nuxt 3 |
本质差异:
- React 假设"一切皆组件",状态更新需要手动 setState;
- Vue 通过 Proxy 自动追踪依赖,更新粒度更细,不需要 Fiber 也能高效更新。