1、React JSX 转换成真实 DOM 的过程?
在 React 中,JSX 转换为真实 DOM 的过程主要分为以下几个核心步骤,涉及 JSX 编译、虚拟 DOM 创建、Diff 算法以及真实 DOM 渲染等环节:
1. JSX 编译为 JavaScript 代码
JSX 本质是 React.createElement 函数的语法糖,并非直接被浏览器解析。
-
编译工具:通过 Babel 等工具将 JSX 语法转换为 React.createElement 调用。
-
转换规则:例如,以下 JSX 代码:
jsx
<div className="container"> <h1>Hello</h1> <p>React</p> </div>会被编译为:
javascript
运行
React.createElement( 'div', // 标签名/组件 { className: 'container' }, // 属性(props) React.createElement('h1', null, 'Hello'), // 子元素1 React.createElement('p', null, 'React') // 子元素2 )
2. 创建虚拟 DOM(Virtual DOM)
React.createElement 函数执行后,会返回一个虚拟 DOM 对象(通常称为 ReactElement)。
-
虚拟 DOM 是对真实 DOM 的轻量 JavaScript 描述,包含标签名(
type)、属性(props)、子元素(children)等信息。 -
示例(简化版虚拟 DOM 结构):
javascript
运行
{ type: 'div', props: { className: 'container', children: [ { type: 'h1', props: { children: 'Hello' } }, { type: 'p', props: { children: 'React' } } ] } } -
对于自定义组件(如
function MyComponent()),type会指向组件函数本身,后续会通过组件渲染逻辑生成更具体的虚拟 DOM。
3. 虚拟 DOM 树的构建
通过递归处理 ReactElement 的 children,React 会构建出完整的虚拟 DOM 树,包含所有层级的节点信息。
4. Diff 算法:计算虚拟 DOM 树的差异
当组件状态(state)或属性(props)变化时,React 会重新生成新的虚拟 DOM 树,然后通过 Diff 算法 对比新旧虚拟 DOM 树,找出需要更新的部分(最小化差异)。
-
Diff 算法的优化策略:
- 按层级比较(只比较同一层级节点,不跨层级对比)。
- 同类型节点比较属性差异,不同类型节点直接替换。
- 列表节点通过
key标识唯一性,避免不必要的重排。
5. 将差异应用到真实 DOM(Commit 阶段)
React 根据 Diff 算法计算出的差异(变更集),通过 ReactDOM 将这些变更批量应用到真实 DOM 上,完成页面更新。
- 这一步是 React 性能优化的关键:通过批量处理 DOM 操作,减少浏览器重绘(Repaint)和重排(Reflow),提升性能。
总结流程
plaintext
JSX → [Babel 编译] → React.createElement 调用 → [执行] → 虚拟 DOM 对象 → [递归构建] → 虚拟 DOM 树 → [Diff 算法] → 计算差异 → [ReactDOM] → 真实 DOM 更新
通过这一过程,React 实现了声明式编程(开发者只需描述 UI 应该是什么样子),并通过虚拟 DOM 和 Diff 算法高效管理真实 DOM 操作。
2、React 服务端渲染(SSR)原理?
React 服务端渲染(SSR,Server-Side Rendering)是指在服务器端将 React 组件渲染为完整的 HTML 字符串,再发送给客户端的技术。其核心目的是解决单页应用(SPA)首屏加载慢、SEO 不友好等问题。以下是其核心原理和流程:
1. SSR 与传统 SPA 的核心区别
- 传统 SPA:客户端加载空 HTML + JavaScript bundle,通过 JS 动态生成 DOM 并渲染页面(客户端渲染,CSR)。缺点:首屏加载依赖 JS 解析执行,速度慢;搜索引擎可能无法抓取动态内容,SEO 差。
- SSR:服务器直接生成包含完整内容的 HTML 发送给客户端,客户端只需 “激活”(hydrate)为可交互的 React 应用。优势:首屏加载快(HTML 直接渲染)、SEO 友好(内容在 HTML 中可见)。
2. SSR 的核心原理
(1)服务器端:渲染 React 组件为 HTML 字符串
-
关键 API:
ReactDOMServer.renderToString()或renderToStaticMarkup()(后者用于静态内容,不含 React 内部属性)。 -
过程:
- 服务器接收客户端请求(如用户访问
https://example.com/home)。 - 根据路由匹配对应的 React 组件(如
Home组件)。 - 获取组件所需的数据(如通过 API 调用、数据库查询)。
- 将数据传入组件,通过
renderToString()把组件渲染为 HTML 字符串。 - 把 HTML 字符串嵌入到一个完整的 HTML 模板中(包含
<html>、<head>、<body>等标签),并将数据附加到 HTML 中(如通过<script>标签注入window.__INITIAL_STATE__,供客户端复用)。 - 服务器将完整 HTML 响应给客户端。
- 服务器接收客户端请求(如用户访问
(2)客户端:激活(Hydration)为可交互应用
客户端接收到服务器返回的 HTML 后,需要执行以下步骤:
-
浏览器直接解析 HTML,快速展示首屏内容(无需等待 JS 加载完成)。
-
客户端加载 React 框架和应用的 JavaScript bundle。
-
通过
ReactDOM.hydrateRoot()(React 18+)将服务器渲染的静态 HTML 与客户端的 React 组件 “关联”,使其变为可交互状态(绑定事件、处理状态更新等)。- 注:
hydrate会复用服务器生成的 DOM 结构,避免重复渲染,仅添加事件监听等交互逻辑。
- 注:
(3)数据同步:前后端数据一致
SSR 必须保证 “服务器渲染的内容” 与 “客户端激活后的内容” 完全一致,否则会导致 Hydration 错误。核心方案:
- 服务器端获取数据后,将数据同步到客户端(如通过
window.__INITIAL_STATE__全局变量)。 - 客户端初始化时,直接从
__INITIAL_STATE__读取数据,避免重复请求,确保前后端数据一致。
3. 典型 SSR 流程(以 Next.js 简化模型为例)
-
用户请求:用户访问
https://example.com/posts/1。 -
服务器处理:
- 路由匹配到
posts/[id]页面组件。 - 调用
getServerSideProps(或类似方法)获取文章 ID=1 的数据。 - 用数据渲染
Post组件为 HTML 字符串:<div><h1>文章标题</h1><p>内容...</p></div>。 - 生成完整 HTML 模板,包含上述内容和
window.__INITIAL_STATE__ = { post: {...} }。
- 路由匹配到
-
客户端渲染:
- 浏览器解析 HTML,展示静态内容。
- 加载客户端 JS,执行
hydrateRoot,复用 DOM 并绑定事件(如点击按钮、路由跳转)。 - 后续交互(如切换页面)由客户端路由接管,类似 SPA。
4. SSR 的关键挑战
- 服务器压力:每个请求都需在服务器渲染 HTML,可能增加服务器负载(需配合缓存优化)。
- 数据获取:需在服务器端提前获取所有组件依赖的数据,逻辑复杂(可通过框架如 Next.js 的
getServerSideProps简化)。 - Hydration 一致性:前后端数据 / 组件结构必须一致,否则会导致渲染不匹配错误。
总结
SSR 的核心是 “服务器生成 HTML 字符串,客户端激活为交互应用”,通过提前输出内容解决首屏速度和 SEO 问题。其流程可概括为:请求 → 服务器获取数据 → 渲染组件为 HTML → 发送 HTML 到客户端 → 客户端激活交互。主流框架(如 Next.js、Remix)已封装 SSR 细节,开发者无需手动处理复杂的渲染和数据同步逻辑。
3、常用的 React Hooks
-
状态钩子 (useState): 用于定义组件的 State,类似类定义中 this.state 的功能 -
useReducer:用于管理复杂状态逻辑的替代方案,类似于 Redux 的 reducer。 -
生命周期钩子 (useEffect): 类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。 -
useLayoutEffect:与 useEffect 类似,但在浏览器完成绘制之前同步执行。 -
useContext: 获取 context 对象,用于在组件树中获取和使用共享的上下文。 -
useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果; -
useMemo: 用于缓存计算结果,避免重复计算昂贵的操作。 -
useRef: 获取组件的真实节点;用于在函数组件之间保存可变的值,并且不会引发重新渲染。 -
useImperativeHandle:用于自定义暴露给父组件的实例值或方法。 -
useDebugValue:用于在开发者工具中显示自定义的钩子相关标签。
4、useEffect VS useLayoutEffect
-
使用场景:useEffect在 React 的渲染过程中是被异步调用的,用于绝大多数场景;useLayoutEffect会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。- 也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
-
使用效果:useEffect是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM后渲染),不会产生闪烁。useLayoutEffect总是比useEffect先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
5、如何理解 React Fiber 架构?
6、简述 React diff 算法过程
7、react18有哪些更新
React 18 是 React 团队经过多年研发的重要版本,核心围绕 “并发渲染(Concurrent Rendering)” 架构展开,带来了多项性能优化和开发体验改进。以下是其核心更新:
一、并发渲染(Concurrent Rendering):核心架构升级
这是 React 18 最底层的变革,允许 React 中断、暂停、恢复甚至放弃渲染工作,为后续的新特性(如自动批处理、Suspense 增强)奠定基础。
- 非阻塞渲染:渲染不再是同步且不可中断的过程,高优先级任务(如用户输入)可以打断低优先级任务(如列表渲染),避免页面卡顿。
- 向后兼容:现有代码无需修改即可运行在 React 18 中,并发特性是 “opt-in”(按需启用)的。
二、自动批处理(Automatic Batching):减少不必要的渲染
批处理是指将多个状态更新合并为一次渲染,减少 DOM 操作次数。React 18 扩展了批处理的范围:
- 传统批处理:仅在 React 事件处理函数(如
onClick)中生效。 - 自动批处理:在
setTimeout、Promise.then、异步函数、自定义事件等场景下,依然会自动合并状态更新。
示例:
jsx
// React 18 中,以下两次 setState 会合并为一次渲染
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
若需强制立即更新(跳出批处理),可使用 flushSync:
jsx
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // 立即渲染
});
setFlag(f => !f); // 单独渲染
三、Transitions API:区分紧急与非紧急更新
用于标记非紧急更新,避免其阻塞用户交互(如输入、点击等紧急操作)。
startTransition:将状态更新标记为 “过渡”,React 会优先处理紧急更新,延迟非紧急更新(必要时可中断并重启)。- 适用场景:搜索框输入时的列表过滤、标签切换时的内容加载等。
示例:
jsx
import { startTransition } from 'react';
function SearchInput({ query, setQuery, setResults }) {
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 紧急更新:立即更新输入框内容
// 非紧急更新:标记为过渡,避免阻塞输入
startTransition(() => {
setResults(search(value)); // 可能耗时的搜索逻辑
});
};
}
四、Suspense 增强:支持服务器端渲染(SSR)和数据获取
React 18 扩展了 Suspense 的能力,使其在 SSR 和客户端数据获取中更强大:
- SSR 流式渲染:服务器可以 “流式” 发送 HTML(先发送页面骨架,再发送异步内容),配合
Suspense实现 “逐步显示”,减少首屏加载时间。 - 客户端数据获取:虽然 React 18 未内置数据获取库,但
Suspense可以与第三方库(如 Relay、React Query 未来版本)配合,在数据加载时显示 fallback(如加载动画),无需手动管理isLoading状态。
示例:
jsx
// 数据加载时显示 "Loading..."
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>
五、新的客户端渲染 API
React 18 废弃了 ReactDOM.render,引入了新的根节点 API,以支持并发渲染:
createRoot:创建根节点,启动并发模式。hydrateRoot:替代ReactDOM.hydrate,用于服务器渲染后的客户端激活。
示例:
jsx
// 旧方式(React 17 及之前)
ReactDOM.render(<App />, document.getElementById('root'));
// 新方式(React 18)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />); // 支持并发渲染
六、服务器组件(Server Components):实验性特性
React 18 引入了服务器组件(Server Components,简称 RSC)的实验性支持,允许组件在服务器端渲染,无需发送 JavaScript 到客户端,进一步减小 bundle 体积。
- 服务器组件不能包含交互逻辑(如
useState、事件处理),专注于渲染静态内容。 - 与客户端组件(Client Components)配合使用,平衡性能与交互性。
七、其他重要更新
- 严格模式(Strict Mode)增强:在开发环境中,会对组件进行两次挂载(模拟卸载后重新挂载),帮助检测副作用问题。
- useId:生成跨服务器和客户端的唯一 ID,解决 SSR 时 hydration 不匹配问题(如表单
id与label的for属性)。 - useTransition 与 useDeferredValue:
useDeferredValue是startTransition的封装,用于为状态创建延迟版本(如输入框联想词的延迟更新)。
总结
React 18 的核心是并发渲染架构,通过自动批处理、Transitions API、Suspense 增强等特性,显著提升了应用的响应速度和用户体验,同时为未来的服务器组件等特性铺平了道路。对于开发者,无需重写现有代码即可升级,同时可逐步按需采用新特性来优化应用。
8、## 简述 React batchUpdate 机制
React 的 batchUpdate(批处理更新)机制 是一种优化策略,旨在将多个状态更新合并为一次渲染,减少不必要的组件重新渲染次数,从而提高性能。
核心机制
- 异步合并更新
当在 同一执行上下文(如同一个事件处理函数、生命周期方法或 React 合成事件)中多次调用状态更新(如setState、useState的setter函数),React 不会立即触发渲染,而是将多个更新收集到一个队列中,最终合并为一次更新,统一计算新状态并渲染。 - 更新队列
React 内部维护一个更新队列。在触发更新的代码块中,所有状态变更会被暂存到队列,直到代码执行完毕,React 才会一次性处理队列中的所有更新,生成新的虚拟 DOM,并通过 Diff 算法高效更新真实 DOM。
触发批处理的场景
-
React 合成事件
如onClick、onChange等事件处理函数中的多次状态更新会自动批处理。jsx 体验AI代码助手 代码解读 复制代码 const handleClick = () => { setCount(1) // 更新入队 setName('Alice') // 更新入队 // 最终合并为一次渲染 } -
React 生命周期函数
在componentDidMount、componentDidUpdate等生命周期方法中的更新会被批处理。 -
React 18+ 的自动批处理增强
React 18 引入createRoot后,即使在异步操作(如setTimeout、Promise、原生事件回调)中的更新也会自动批处理:jsx 体验AI代码助手 代码解读 复制代码 setTimeout(() => { setCount(1) // React 18 中自动批处理 setName('Alice') // 合并为一次渲染 }, 1000)
绕过批处理的场景
-
React 17 及之前的异步代码
在setTimeout、Promise或原生事件回调中的更新默认不会批处理,每次setState触发一次渲染:jsx 体验AI代码助手 代码解读 复制代码 // React 17 中会触发两次渲染 setTimeout(() => { setCount(1) // 渲染一次 setName('Alice') // 渲染第二次 }, 1000) -
手动强制同步更新
使用flushSync(React 18+)可强制立即更新,绕过批处理:jsx 体验AI代码助手 代码解读 复制代码 import { flushSync } from 'react-dom' flushSync(() => { setCount(1) // 立即渲染 }) setName('Alice') // 再次渲染
设计目的
- 性能优化
避免频繁的 DOM 操作,减少浏览器重绘和回流,提升应用性能。 - 状态一致性
确保在同一个上下文中多次状态变更后,组件最终基于最新的状态值渲染,避免中间状态导致的 UI 不一致。
9、React 组件渲染和更新的全过程
React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。以下是 React 组件渲染和更新的全过程,结合源码逻辑和关键步骤进行详细分析。
1. 整体流程概述 React 的渲染和更新过程可以分为以下几个阶段:
- 初始化阶段:创建 Fiber 树和 Hooks 链表。
- 渲染阶段:生成新的虚拟 DOM(Fiber 树)。
- 协调阶段:对比新旧 Fiber 树,找出需要更新的部分。
- 提交阶段:将更新应用到真实 DOM。
- 清理阶段:重置全局变量,准备下一次更新。
2. 详细流程分析
(1)初始化阶段
-
触发条件:组件首次渲染或状态/属性更新。
-
关键函数:
render、createRoot、scheduleUpdateOnFiber。 -
逻辑:
- 通过
ReactDOM.render或createRoot初始化应用。 - 创建根 Fiber 节点(
HostRoot)。 - 调用
scheduleUpdateOnFiber,将更新任务加入调度队列。
- 通过
(2)渲染阶段
-
触发条件:调度器开始执行任务。
-
关键函数:
performSyncWorkOnRoot、beginWork、renderWithHooks。 -
逻辑:
- 调用
performSyncWorkOnRoot,开始渲染任务。 - 调用
beginWork,递归处理 Fiber 节点。 - 对于函数组件,调用
renderWithHooks,执行组件函数并生成新的 Hooks 链表。 - 对于类组件,调用
instance.render,生成新的虚拟 DOM。 - 对于 Host 组件(如
div),生成对应的 DOM 节点。
- 调用
(3)协调阶段
-
触发条件:新的虚拟 DOM 生成后。
-
关键函数:
reconcileChildren、diff。 -
逻辑:
- 调用
reconcileChildren,对比新旧 Fiber 节点。 - 根据
diff算法,找出需要更新的节点。 - 为需要更新的节点打上
Placement、Update、Deletion等标记。
- 调用
(4)提交阶段
-
触发条件:协调阶段完成后。
-
关键函数:
commitRoot、commitWork。 -
逻辑:
- 调用
commitRoot,开始提交更新。 - 调用
commitWork,递归处理 Fiber 节点。 - 根据节点的标记,执行 DOM 操作(如插入、更新、删除)。
- 调用生命周期钩子(如
componentDidMount、componentDidUpdate)。
- 调用
(5)清理阶段
-
触发条件:提交阶段完成后。
-
关键函数:
resetHooks、resetContext。 -
逻辑:
- 重置全局变量(如
currentlyRenderingFiber、currentHook)。 - 清理上下文和副作用。
- 准备下一次更新。
- 重置全局变量(如
10、为何 React Hooks 不能放在条件或循环之内?
一个组件中的hook会以链表的形式串起来, FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个Hook。
在更新时,会复用之前的 Hook,如果通过了条件或循环语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。
核心结论是:React Hooks 依赖调用顺序的稳定性来关联组件状态和 Hooks 逻辑,条件 / 循环会破坏这种顺序,导致状态错乱或报错。
核心原因:Hooks 的 “索引关联” 机制
React 内部通过数组存储组件的 Hooks 状态,每个 Hook 的调用顺序决定了它对应的数组索引。 每次组件渲染(包括初始渲染和更新),Hooks 必须按完全相同的顺序调用,React 才能通过索引找到对应的状态(如 useState 的值、useEffect 的依赖和清理函数)。
条件 / 循环会破坏顺序的具体影响 初始渲染与更新时顺序不一致 例:条件判断导致某轮渲染少调用一个 Hook,后续 Hooks 的索引全部错位。 后果:React 无法匹配 Hooks 与对应的状态,直接抛出 Invalid hook call 错误,或读取到错误的状态值。
状态复用与清理逻辑失效 例:循环中调用 useEffect,循环次数变化会导致部分 useEffect 的清理函数无法正确执行(索引对应错误)。 后果:出现内存泄漏(如未取消的订阅)、重复请求等问题。 示例:错误用法与正确用法对比 错误(条件内调用 Hook): jsx if (isShow) { useEffect(() => { /* 副作用 / }, []); // 可能被跳过,破坏顺序 } 正确(Hook 外层判断条件): jsx useEffect(() => { if (isShow) { / 副作用 */ } // 条件放在 Hook 内部 }, [isShow]);
总结 React Hooks 的设计规则(不放在条件 / 循环 / 嵌套函数内),本质是为了保证 “调用顺序稳定”,让 React 能可靠地关联 Hooks 与组件状态。违反规则会直接导致框架内部状态管理逻辑混乱,引发报错或隐性 Bug。