react

33 阅读17分钟

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 字符串
  • 关键 APIReactDOMServer.renderToString() 或 renderToStaticMarkup()(后者用于静态内容,不含 React 内部属性)。

  • 过程:

    1. 服务器接收客户端请求(如用户访问 https://example.com/home)。
    2. 根据路由匹配对应的 React 组件(如 Home 组件)。
    3. 获取组件所需的数据(如通过 API 调用、数据库查询)。
    4. 将数据传入组件,通过 renderToString() 把组件渲染为 HTML 字符串。
    5. 把 HTML 字符串嵌入到一个完整的 HTML 模板中(包含 <html><head><body> 等标签),并将数据附加到 HTML 中(如通过 <script> 标签注入 window.__INITIAL_STATE__,供客户端复用)。
    6. 服务器将完整 HTML 响应给客户端。
(2)客户端:激活(Hydration)为可交互应用

客户端接收到服务器返回的 HTML 后,需要执行以下步骤:

  1. 浏览器直接解析 HTML,快速展示首屏内容(无需等待 JS 加载完成)。

  2. 客户端加载 React 框架和应用的 JavaScript bundle。

  3. 通过 ReactDOM.hydrateRoot()(React 18+)将服务器渲染的静态 HTML 与客户端的 React 组件 “关联”,使其变为可交互状态(绑定事件、处理状态更新等)。

    • 注:hydrate 会复用服务器生成的 DOM 结构,避免重复渲染,仅添加事件监听等交互逻辑。
(3)数据同步:前后端数据一致

SSR 必须保证 “服务器渲染的内容” 与 “客户端激活后的内容” 完全一致,否则会导致 Hydration 错误。核心方案:

  • 服务器端获取数据后,将数据同步到客户端(如通过 window.__INITIAL_STATE__ 全局变量)。
  • 客户端初始化时,直接从 __INITIAL_STATE__ 读取数据,避免重复请求,确保前后端数据一致。

3. 典型 SSR 流程(以 Next.js 简化模型为例)

  1. 用户请求:用户访问 https://example.com/posts/1

  2. 服务器处理

    • 路由匹配到 posts/[id] 页面组件。
    • 调用 getServerSideProps(或类似方法)获取文章 ID=1 的数据。
    • 用数据渲染 Post 组件为 HTML 字符串:<div><h1>文章标题</h1><p>内容...</p></div>
    • 生成完整 HTML 模板,包含上述内容和 window.__INITIAL_STATE__ = { post: {...} }
  3. 客户端渲染

    • 浏览器解析 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)中生效。
  • 自动批处理:在 setTimeoutPromise.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 和客户端数据获取中更强大:

  1. SSR 流式渲染:服务器可以 “流式” 发送 HTML(先发送页面骨架,再发送异步内容),配合 Suspense 实现 “逐步显示”,减少首屏加载时间。
  2. 客户端数据获取:虽然 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 与 useDeferredValueuseDeferredValue 是 startTransition 的封装,用于为状态创建延迟版本(如输入框联想词的延迟更新)。

总结

React 18 的核心是并发渲染架构,通过自动批处理、Transitions API、Suspense 增强等特性,显著提升了应用的响应速度和用户体验,同时为未来的服务器组件等特性铺平了道路。对于开发者,无需重写现有代码即可升级,同时可逐步按需采用新特性来优化应用。

8、## 简述 React batchUpdate 机制

React 的 batchUpdate(批处理更新)机制 是一种优化策略,旨在将多个状态更新合并为一次渲染,减少不必要的组件重新渲染次数,从而提高性能。

核心机制

  1. 异步合并更新
    当在 同一执行上下文(如同一个事件处理函数、生命周期方法或 React 合成事件)中多次调用状态更新(如 setStateuseStatesetter 函数),React 不会立即触发渲染,而是将多个更新收集到一个队列中,最终合并为一次更新,统一计算新状态并渲染。
  2. 更新队列
    React 内部维护一个更新队列。在触发更新的代码块中,所有状态变更会被暂存到队列,直到代码执行完毕,React 才会一次性处理队列中的所有更新,生成新的虚拟 DOM,并通过 Diff 算法高效更新真实 DOM。

触发批处理的场景

  1. React 合成事件
    onClickonChange 等事件处理函数中的多次状态更新会自动批处理。

    jsx
     体验AI代码助手
     代码解读
    复制代码
    const handleClick = () => {
      setCount(1) // 更新入队
      setName('Alice') // 更新入队
      // 最终合并为一次渲染
    }
    
  2. React 生命周期函数
    componentDidMountcomponentDidUpdate 等生命周期方法中的更新会被批处理。

  3. React 18+ 的自动批处理增强
    React 18 引入 createRoot 后,即使在异步操作(如 setTimeoutPromise、原生事件回调)中的更新也会自动批处理:

    jsx
     体验AI代码助手
     代码解读
    复制代码
    setTimeout(() => {
      setCount(1) // React 18 中自动批处理
      setName('Alice') // 合并为一次渲染
    }, 1000)
    

绕过批处理的场景

  1. React 17 及之前的异步代码
    setTimeoutPromise 或原生事件回调中的更新默认不会批处理,每次 setState 触发一次渲染:

    jsx
     体验AI代码助手
     代码解读
    复制代码
    // React 17 中会触发两次渲染
    setTimeout(() => {
      setCount(1) // 渲染一次
      setName('Alice') // 渲染第二次
    }, 1000)
    
  2. 手动强制同步更新
    使用 flushSync(React 18+)可强制立即更新,绕过批处理:

    jsx
     体验AI代码助手
     代码解读
    复制代码
    import { flushSync } from 'react-dom'
    
    flushSync(() => {
      setCount(1) // 立即渲染
    })
    setName('Alice') // 再次渲染
    

设计目的

  1. 性能优化
    避免频繁的 DOM 操作,减少浏览器重绘和回流,提升应用性能。
  2. 状态一致性
    确保在同一个上下文中多次状态变更后,组件最终基于最新的状态值渲染,避免中间状态导致的 UI 不一致。

9、React 组件渲染和更新的全过程

React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。以下是 React 组件渲染和更新的全过程,结合源码逻辑和关键步骤进行详细分析。

1. 整体流程概述 React 的渲染和更新过程可以分为以下几个阶段:

  1. 初始化阶段:创建 Fiber 树和 Hooks 链表。
  2. 渲染阶段:生成新的虚拟 DOM(Fiber 树)。
  3. 协调阶段:对比新旧 Fiber 树,找出需要更新的部分。
  4. 提交阶段:将更新应用到真实 DOM。
  5. 清理阶段:重置全局变量,准备下一次更新。

2. 详细流程分析

(1)初始化阶段

  • 触发条件:组件首次渲染或状态/属性更新。

  • 关键函数rendercreateRootscheduleUpdateOnFiber

  • 逻辑

    1. 通过 ReactDOM.rendercreateRoot 初始化应用。
    2. 创建根 Fiber 节点(HostRoot)。
    3. 调用 scheduleUpdateOnFiber,将更新任务加入调度队列。

(2)渲染阶段

  • 触发条件:调度器开始执行任务。

  • 关键函数performSyncWorkOnRootbeginWorkrenderWithHooks

  • 逻辑

    1. 调用 performSyncWorkOnRoot,开始渲染任务。
    2. 调用 beginWork,递归处理 Fiber 节点。
    3. 对于函数组件,调用 renderWithHooks,执行组件函数并生成新的 Hooks 链表。
    4. 对于类组件,调用 instance.render,生成新的虚拟 DOM。
    5. 对于 Host 组件(如 div),生成对应的 DOM 节点。

(3)协调阶段

  • 触发条件:新的虚拟 DOM 生成后。

  • 关键函数reconcileChildrendiff

  • 逻辑

    1. 调用 reconcileChildren,对比新旧 Fiber 节点。
    2. 根据 diff 算法,找出需要更新的节点。
    3. 为需要更新的节点打上 PlacementUpdateDeletion 等标记。

(4)提交阶段

  • 触发条件:协调阶段完成后。

  • 关键函数commitRootcommitWork

  • 逻辑

    1. 调用 commitRoot,开始提交更新。
    2. 调用 commitWork,递归处理 Fiber 节点。
    3. 根据节点的标记,执行 DOM 操作(如插入、更新、删除)。
    4. 调用生命周期钩子(如 componentDidMountcomponentDidUpdate)。

(5)清理阶段

  • 触发条件:提交阶段完成后。

  • 关键函数resetHooksresetContext

  • 逻辑

    1. 重置全局变量(如 currentlyRenderingFibercurrentHook)。
    2. 清理上下文和副作用。
    3. 准备下一次更新。

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。