React 09
1 组件返回它们的 JSX
保持组件纯粹 – React 中文文档
https://zh-hans.react.dev/learn/keeping-components-pure
在 React 等基于 JSX 的前端框架中,“组件返回它们的 JSX” 指的是:组件作为一个函数或类,其核心职责是根据输入的 props 和自身状态(state),计算并返回一段 JSX 结构,这段结构描述了该组件在页面上应该如何渲染。
简单来说,JSX 是一种类似 HTML 的语法扩展,用于描述 UI 结构(比如 <div>Hello</div>)。组件的作用就是 “产出” 这段描述性的 JSX,框架(如 React)会根据这段 JSX 最终渲染出真实的 DOM 元素。
举个例子:
一个简单的函数组件:
function Greeting(props) {// 组件的核心逻辑:根据 props 计算并返回 JSXreturn <h1>Hello, {props.name}!</h1>;
}
这里的 return <h1>...</h1> 就是 “组件返回 JSX”—— 它告诉框架:“我需要渲染一个 <h1> 标签,内容是 Hello, [name]!”。
为什么强调 “只返回 JSX,不修改已有对象 / 变量”?
这是为了保证组件的 “纯粹性”(纯函数特性):
- 纯函数的核心特点是:输入相同,输出一定相同,且不会产生副作用(比如修改外部变量、修改传入的 props 对象等)。
- 如果组件在返回 JSX 之外,还偷偷修改了外部变量或已有对象(比如修改全局变量、修改父组件传入的 props),就会导致组件行为不可预测,难以调试(比如多次渲染结果不一致,或其他组件受意外影响)。
例如,下面的组件就是 “不纯粹” 的,因为它修改了外部变量:
let globalCount = 0;function BadComponent() {globalCount++; // 副作用:修改了外部变量return <div>Count: {globalCount}</div>;
}
这个组件每次渲染都会改变 globalCount,导致多次渲染的结果依赖于渲染次数,而不是输入的 props/state,违背了纯函数原则。
总结:“组件返回 JSX” 是指组件的核心职责是产出描述 UI 的结构,而不是去修改外部状态或已有对象,这样才能保证组件的可预测性和可维护性。

严格模式
这是关于React 严格模式检测不纯计算的说明内容,可从以下几点解读:
1. 渲染输入的只读性
在 React 渲染时,props、state 和 context 这三种输入应视为只读,不能在渲染过程中直接修改。若需更改内容,需通过 “设置状态” 的方式,而非直接写入变量。
2. 严格模式的作用
React 的严格模式(<React.StrictMode>)会在开发时重复调用组件函数两次,以此检测违反 “纯函数” 规则的组件(即那些在渲染时修改预先存在的变量或对象的组件)。它仅在开发环境生效,不会影响生产环境的性能。
3. 纯函数的体现
纯函数组件的特点是 “相同输入总是返回相同输出,且无副作用”。例如文中的纯函数版本,即使被调用两次,结果也不会出错,就像数学函数 double(2) 或 y = 2x 多次计算结果始终一致。
2 局部 mutation(局部可变操作)

在 React 等前端开发语境中,“局部 mutation(局部可变操作)” 指的是在组件内部对仅属于自身的变量进行修改,且这种修改不会影响组件外部的状态或其他组件。它的作用可以从以下几点理解:
1. 用于组件内部的临时状态管理
当组件需要一些 “临时、仅自己用” 的状态时,局部 mutation 可以派上用场。比如:
function MyComponent() {// 局部变量,仅在组件内部生效,修改它属于局部 mutationlet localCount = 0; const handleClick = () => {localCount++; // 局部 mutation,只改变组件内部的这个变量console.log(localCount);};return <button onClick={handleClick}>点击</button>;
}
这里的 localCount 是组件内部的临时变量,修改它不会影响外部,可用于记录一些不需要 “跨渲染保存” 的临时状态(比如单次交互中的中间值)。
2. 优化性能(避免不必要的重渲染)
对于一些不需要触发组件重渲染的操作,局部 mutation 可以避免因状态变更导致的性能损耗。例如,组件内部的缓存计算、临时数据的组装:
function DataList({ items }) {// 局部变量,用于缓存过滤后的结果let filteredItems = []; const filterData = (keyword) => {filteredItems = items.filter(item => item.includes(keyword)); // 局部 mutation// 后续直接使用 filteredItems,无需触发组件重渲染};// ...
}
这里修改 filteredItems 是局部操作,不会让组件重新渲染,适合处理那些不需要界面响应的内部逻辑。
3. 区分 “纯渲染” 与 “副作用”
React 要求组件的渲染逻辑(返回 JSX 的过程)必须是纯函数(无副作用、不修改外部状态),但组件内部的 “非渲染逻辑”(如事件处理函数、 useEffect 中的逻辑)可以包含局部 mutation。这样既保证了渲染的可预测性,又能在组件内部灵活处理业务逻辑。
简单来说,“局部 mutation” 就像组件的 “内部小秘密”—— 只在自己的 “地盘” 里折腾,不对外界造成影响,从而在保证组件纯度的前提下,满足内部临时状态管理、性能优化等需求。
【会导致渲染结果是随机的吗?】
“局部 mutation” 虽然是组件内部的 “小秘密”,但只要控制在“仅作用于组件内部、不影响渲染逻辑” 的范围内,就不会导致渲染结果随机。
它的使用是有明确场景和约束的,我们可以从两个角度理解:
一、局部 mutation 不会让渲染随机的原因
React 组件的渲染结果由 props、state、context 决定(这三者是 “渲染的输入”,必须保持只读)。而局部 mutation 操作的是组件内部的 “临时变量”,这些变量不会参与 JSX 的渲染计算,所以不会影响最终的 UI 输出。
举个例子:
function Timer() {// 局部变量,用于内部计时(属于局部 mutation)let localTimer = 0; useEffect(() => {const interval = setInterval(() => {localTimer++; // 局部 mutation,只改内部变量console.log('内部计时:', localTimer);}, 1000);return () => clearInterval(interval);}, []);// 渲染的 JSX 只依赖“无 mutation”的结构,所以渲染结果是稳定的return <div>我是一个定时器组件</div>;
}
这里 localTimer 的修改是局部的,既不影响 JSX 渲染,也不触发组件重渲染,所以渲染结果始终是 “我是一个定时器组件”,完全可预测。
二、局部 mutation 的安全使用场景(保证不影响渲染)
它的 “能干啥” 是很明确的,只要遵循以下场景,就不会有随机性:
- 临时状态: 存储组件内部的临时中间值(如单次交互的临时标记、未提交的表单草稿)。
- 性能优化: 缓存计算结果、组装临时数据(如过滤列表时的中间结果,不需要界面实时响应)。
- 副作用逻辑: 在
useEffect、事件处理函数等 “非渲染逻辑” 中,处理仅组件内部需要的操作(如日志记录、第三方库的局部配置)。
总结一下:只要你把 “局部 mutation” 限定在 “组件内部的非渲染逻辑” 中,它就只是一个 “可控的内部操作”,不会让渲染结果随机。
3 React侧重纯函数

4 如何判断一个组件是否是“纯粹”的?
判断一个 React 组件是否 “纯粹”,核心是看它是否满足“输入确定则输出唯一,且无副作用”的原则。可以通过以下 3 个关键标准逐层验证,确保判断逻辑清晰且全面。
核心判断标准
1. 输入依赖仅为 props、state、context
组件的所有渲染逻辑、计算逻辑,只能依赖这三类 “官方输入源”,不能依赖外部变量、全局状态、随机值等不可控因素。
- 符合要求的情况:
- 基于
props.name拼接字符串。 - 根据
state.count计算count * 2。 - 通过
useContext获取主题色并应用到样式。
- 基于
- 不符合要求的情况:
- 依赖组件外部定义的
let globalNum = 10进行计算。 - 使用
Math.random()生成渲染内容(输入固定时输出随机)。 - 读取
window.innerWidth等浏览器全局变量(组件外环境变化会影响输出)。
- 依赖组件外部定义的
2. 无 “副作用” 操作
组件在执行过程中(包括渲染阶段、计算阶段),不能修改任何外部状态或自身输入源,也不能执行影响外部环境的操作。
- 需规避的 “副作用” 行为:
- 修改传入的
props(如props.name = "newName")。 - 修改组件外部的变量或对象(如全局数组
window.list.push(1))。 - 在渲染阶段执行
fetch请求、操作 DOM、修改document.title等。 - 直接修改
state(如this.state.count++,需通过setState或useState的更新函数)。
- 修改传入的
3. 输入相同则输出(JSX)必然唯一
只要 props、state、context 这三类输入完全一致,组件返回的 JSX 结构、触发的逻辑就必须完全相同,不存在 “同输入不同输出” 的情况。
- 符合要求的情况:
- 输入
props.isShow = true时,始终渲染<div>显示内容</div>。 - 输入
state.num = 5时,计算结果num * 3始终为 15。
- 输入
- 不符合要求的情况:
- 组件内部用
let localNum = 0记录次数,每次渲染localNum++后嵌入 JSX(同输入下,多次渲染结果不同)。 - 用
Date.now()生成时间戳并嵌入 JSX(即使输入相同,不同时间渲染结果不同)。
- 组件内部用
快速验证方法:“两次渲染测试”
可以模拟 React 严格模式的逻辑,通过 “同一输入下执行两次组件” 来验证纯粹性:
- 固定一组
props、state作为输入。 - 连续两次调用组件函数,观察返回的 JSX 及执行过程。
- 若两次结果完全一致,且无外部状态被修改,则组件是纯粹的;反之则不纯粹。
示例验证:
// 纯粹组件
function PureComponent({ num }) {const double = num * 2;return <div>{double}</div>;
}
// 两次输入 num=3,均返回 <div>6</div> → 纯粹// 不纯粹组件
let localCount = 0;
function ImpureComponent() {localCount++;return <div>{localCount}</div>;
}
// 两次调用,分别返回 <div>1</div>、<div>2</div> → 不纯粹
总结
判断组件是否纯粹,本质是看它是否 “可控、可预测”。只要满足 **“依赖输入唯一、无副作用、输出确定”** 这三个标准,就是纯粹组件。这类组件更易于调试、复用,也能更好地配合 React 的性能优化机制(如 React.memo)。
5 优化纯粹的组件
export default function Clock({ time }) {const hours = time.getHours();if (hours >= 0 && hours <= 6) {document.getElementById('time').className = 'night';} else {document.getElementById('time').className = 'day';}return (<h1 id="time">{time.toLocaleTimeString()}</h1>);
}
【document.getElementById('time').className = 'day'; 原本这个想表达什么?】
这句代码的核心意图是给页面中 id 为 "time" 的 DOM 元素,设置一个名为 "day" 的 CSS 类名,本质是通过原生 JavaScript 手动操作 DOM 来控制元素的样式。
代码意图拆解
-
获取目标 DOM 元素:
document.getElementById('time')这部分代码的作用是从整个文档(document)中,找到并获取id属性值为 "time" 的 DOM 元素。在你之前的Clock组件里,目标就是返回的<h1 id="time">这个标题元素。 -
设置 CSS 类名:
.className = 'day'这部分是给获取到的 DOM 元素赋值,将其className属性(对应 HTML 中的class属性)设置为 "day"。- 前端开发中,
className的核心用途是关联 CSS 样式。当元素拥有 "day" 类名后,会自动应用 CSS 中预先定义的.day选择器对应的样式(比如设置字体颜色、背景色等,以体现 “白天” 相关的视觉效果)。
- 前端开发中,
-
结合组件逻辑的完整意图结合你之前
Clock组件的逻辑,这句代码是想根据时间判断,手动给时钟元素切换 “白天” 样式:- 当时间不在凌晨 0-6 点时,通过该代码给
id="time"的元素加 "day" 类,让元素显示白天的样式; - 反之则加 "night" 类,显示夜晚的样式。
- 当时间不在凌晨 0-6 点时,通过该代码给
为什么在 React 组件中不推荐这么写
React 的设计理念是 “声明式” 管理 UI,即开发者只需通过 JSX 描述 “想要的 UI 是什么样”,由 React 内部负责 DOM 的创建和修改,无需手动操作 DOM。
- 手动用
document.getElementById操作 DOM,会绕过 React 对 DOM 的管理,可能导致 React 维护的组件状态与实际 DOM 状态不一致(比如样式混乱、重复修改等)。 - 更合理的方式是像优化后的代码那样,通过计算得到
className,再通过 JSX 的className属性绑定到元素上,让 React 统一控制样式,既符合组件纯粹性要求,也更安全可靠。
分析并优化
这个 Clock 组件不是纯粹的组件,核心问题在于它在渲染阶段直接操作了 DOM,违反了 React 组件 “无副作用” 的纯粹性原则。
一、组件不纯粹的核心原因
1. 渲染阶段存在 “副作用” 操作
组件在执行过程中(return 之前)调用了 document.getElementById('time').className = 'xxx',这属于直接操作 DOM 的 “副作用” 行为。
- React 要求渲染阶段(组件函数执行过程)必须是纯函数,只能进行计算、判断等无副作用操作,不能修改 DOM、发送请求、修改外部变量等。
- 该组件在渲染时直接操作尚未完全挂载的 DOM 元素,不仅破坏纯粹性,还可能因 DOM 未渲染完成导致
getElementById获取到null,引发报错。
2. 逻辑与 React 渲染机制冲突
React 组件的 UI 应由 JSX 描述,样式应通过 JSX 中的 className 属性绑定,而非手动操作 DOM。
- 组件的
return已经返回了<h1 id="time">,但又在渲染前手动修改该元素的类名,相当于 “重复操作”,违背了 React 对 UI 的统一管理逻辑。 - 若组件因
time变化重新渲染,手动修改的类名可能与 JSX 描述的状态不一致,导致样式混乱。
二、优化方案:改造为纯粹组件
要让组件恢复纯粹性,需移除渲染阶段的副作用,通过 React 原生方式绑定样式。优化后的代码如下:
export default function Clock({ time }) {const hours = time.getHours();// 1. 仅通过计算得到 className,无副作用操作const className = hours >= 0 && hours <= 6 ? 'night' : 'day';// 2. 通过 JSX 的 className 属性绑定样式,由 React 统一管理 DOMreturn (<h1 id="time" className={className}>{time.toLocaleTimeString()}</h1>);
}
三、优化后组件的纯粹性验证
- 输入依赖唯一:仅依赖传入的
props.time,无其他外部依赖。 - 无副作用:渲染过程中仅计算
className,未修改 DOM、外部变量等,符合纯函数要求。 - 输入输出确定:只要
time相同,hours计算结果就相同,className和返回的 JSX 也完全一致,输出唯一可预测。
四、关键结论
React 组件的纯粹性核心是 “渲染阶段只做计算,不做修改”。UI 相关的状态(如样式、结构)应通过 “计算结果 → 嵌入 JSX” 的方式由 React 统一渲染,而非手动操作 DOM 破坏这一流程。
6 什么是DOM
DOM 是 Document Object Model(文档对象模型) 的缩写,它是浏览器将 HTML/XML 文档解析后生成的树形结构对象模型,本质是一套让程序(如 JavaScript)能访问、操作网页内容和结构的接口。
简单说,DOM 把网页的 HTML 结构 “翻译” 成 JavaScript 能看懂的 “对象”,让你可以通过代码修改网页的内容、样式和结构。
一、DOM 的核心作用:连接 HTML 与 JavaScript
浏览器加载 HTML 后,会自动将 HTML 标签转化为 DOM 树中的节点(Node),每个节点对应一个 HTML 元素、属性或文本。
举个例子:
一段简单的 HTML 代码:
<!DOCTYPE html>
<html><body><h1 id="time" class="day">12:00:00</h1></body>
</html>
浏览器解析后生成的 DOM 树结构(简化):
document(根对象,代表整个文档)html节点(根元素节点)body节点(子元素节点)h1节点(子元素节点)- 属性节点:
id="time"、class="day" - 文本节点:
12:00:00
- 属性节点:
通过 DOM 接口,JavaScript 就能 “找到” 并 “操作” 这个 h1 节点,比如修改它的文本、样式或属性。
二、DOM 的常见操作(通过 JavaScript 实现)
开发者主要通过 document 对象提供的方法操作 DOM,核心操作可分为四类:
| 操作类型 | 常见方法示例 | 作用说明 |
|---|---|---|
| 查找节点 | document.getElementById('time') | 根据 id 找到指定元素节点 |
document.querySelector('.day') | 根据 CSS 选择器找到第一个匹配的元素节点 | |
| 修改内容 | element.textContent = '13:00:00' | 修改元素的文本内容 |
element.innerHTML = '<span>13:00</span>' | 修改元素内部的 HTML 结构 | |
| 修改样式 | element.className = 'night' | 修改元素的 CSS 类名 |
element.style.color = 'white' | 直接修改元素的行内样式 | |
| 修改结构 | document.createElement('div') | 创建一个新的元素节点 |
parent.appendChild(child) | 给父节点添加子节点(插入元素) | |
parent.removeChild(child) | 从父节点中删除子节点(移除元素) |
三、DOM 与 React 的关系
React 的核心设计理念之一是 **“虚拟 DOM(Virtual DOM)”**,它是对真实 DOM 的 “轻量副本”。
-
React 为何不推荐直接操作真实 DOM?
- 真实 DOM 操作性能开销大,频繁修改可能导致页面卡顿。
- 直接操作 DOM 会绕过 React 对页面状态的管理,容易出现 “React 状态与真实 DOM 不一致” 的问题(比如你之前组件中手动修改
className的情况)。
-
React 的处理方式
- 开发者通过 JSX 描述 UI 结构,React 内部将其转化为虚拟 DOM。
- 当数据(props/state)变化时,React 会对比新旧虚拟 DOM 的差异,只更新真实 DOM 中需要变化的部分,既保证了 UI 与状态一致,又优化了性能。
总结
DOM 是浏览器提供的 “桥梁”,让 JavaScript 能操作网页;而 React 则通过虚拟 DOM 对真实 DOM 操作进行了 “优化和封装”,让开发者更专注于数据和 UI 的关系,无需手动频繁操作真实 DOM。
DOM 核心概念、操作方法,以及与 React 实现方式的对比表
一、DOM 核心概念
| 概念 | 说明 |
|---|---|
| 定义 | Document Object Model(文档对象模型),浏览器将 HTML 解析为树形结构的对象模型,供程序(如 JS)操作。 |
| 本质 | 一套接口(API),连接 HTML 与 JavaScript,使 JS 能修改网页内容、样式、结构。 |
| 核心结构 | 以 document 为根的树形节点结构,每个节点对应 HTML 元素、属性或文本。 |
| 与 React 的关系 | React 通过 “虚拟 DOM(Virtual DOM)” 间接操作真实 DOM,优化性能并统一状态管理。 |
二、常见 DOM 操作及 React 对应实现
| 操作类型 | 原生 DOM 操作(JavaScript) | React 实现方式(JSX + Hooks) | 核心差异 |
|---|---|---|---|
| 查找元素 | document.getElementById('id') | 无需手动查找,通过 JSX 直接绑定数据(或用 ref 特殊场景) | React 中 UI 由数据驱动,无需手动获取 DOM 元素,ref 仅用于极少数必须操作 DOM 的场景(如聚焦输入框)。 |
document.querySelector('.class') | |||
| 修改文本 | element.textContent = '新内容' | <div>{变量}</div>(变量更新时自动渲染) | React 通过 “数据变化 → 重新渲染 JSX” 实现文本更新,无需手动操作 DOM。 |
| 修改样式 | element.className = 'active'(类名) | <div className={条件 ? 'active' : ''}> | React 将样式类名通过 JSX 属性绑定,由 React 自动同步到 DOM。 |
element.style.color = 'red'(行内样式) | <div style={{ color: 'red' }}> | React 行内样式需用对象形式(驼峰命名,如 fontSize),而非字符串。 | |
| 修改结构 | document.createElement('div')(创建元素) | 直接在 JSX 中写元素,如 <div>新内容</div> | React 中 UI 结构由 JSX 声明式描述,无需手动创建 / 插入 DOM 节点。 |
parent.appendChild(child)(添加子元素) | 用数组渲染列表:{list.map(item => <li key={item.id}>{item}</li>)} | React 通过数组 map 生成列表,自动处理 DOM 增删,需加 key 优化性能。 | |
element.remove()(删除元素) | 用条件渲染:{show && <div>显示内容</div>} | React 通过布尔值控制元素是否渲染,无需手动删除 DOM。 | |
| 事件绑定 | element.addEventListener('click', handleClick) | <button onClick={handleClick}> | React 事件用驼峰命名(如 onClick),直接绑定函数,无需手动添加 / 移除监听器。 |
三、关键结论
- DOM 是浏览器提供的原生接口,允许直接操作网页元素,但手动操作繁琐且可能导致性能问题。
- React 封装了 DOM 操作,通过 “数据驱动 + 声明式 JSX” 让开发者专注于 “UI 应该是什么样”,而非 “如何操作 DOM 实现”。
- 开发 React 时,应优先使用 React 提供的方式(如状态管理、条件渲染、样式绑定),避免直接调用
document相关 API 操作 DOM,否则可能破坏组件纯粹性和状态一致性。
7 不纯粹的组件改错
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';let currentPerson;export default function Profile({ person }) {currentPerson = person;return (<Panel><Header /><Avatar /></Panel>)
}function Header() {return <h1>{currentPerson.name}</h1>;
}function Avatar() {return (<imgclassName="avatar"src={getImageUrl(currentPerson)}alt={currentPerson.name}width={50}height={50}/>);
}
这个组件不是纯粹的,核心问题在于它通过全局变量 currentPerson 在组件间传递数据,违反了 React 组件 “依赖输入唯一、无副作用” 的纯粹性原则,可能导致不可预测的渲染结果。
一、组件不纯粹的具体原因
1. 存在 “副作用”:修改外部全局变量
Profile 组件在渲染过程中执行了 currentPerson = person,这是对组件外部定义的 let currentPerson 进行修改,属于副作用操作。React 要求组件渲染阶段(函数执行过程)只能做计算,不能修改外部状态(包括全局变量、父组件传入的 props 等),否则会破坏组件的可预测性。
2. 依赖非标准输入源:全局变量
Header 和 Avatar 组件没有通过 props 接收数据,而是直接依赖全局变量 currentPerson 渲染内容。这会导致:
- 组件的输入来源不明确(无法通过函数参数看出依赖什么数据)。
- 若
currentPerson被其他代码意外修改(比如其他组件也操作这个变量),Header和Avatar的渲染结果会突然变化,难以调试。
3. 同输入可能不同输出:违反确定性
假设 Profile 组件因父组件更新而重新渲染,即使传入的 person 相同,currentPerson 也会被重复赋值(虽然值没变,但逻辑上仍属于不必要的副作用)。更严重的是:如果 React 在严格模式下对组件进行 “双重渲染”(开发环境检测不纯组件的机制),可能会导致 currentPerson 状态混乱,最终 Header 和 Avatar 渲染出错误内容。
二、优化方案:通过 props 传递数据(改造为纯粹组件)
React 中组件间传递数据的标准方式是 props,它能保证数据流向清晰、组件依赖明确。优化后的代码如下:
import Panel from './Panel.js';
import { getImageUrl } from './utils.js';// 移除全局变量 currentPersonexport default function Profile({ person }) {// 不再修改外部变量,仅通过 props 向下传递数据return (<Panel><Header name={person.name} /> {/* 通过 props 传值给 Header */}<Avatar person={person} /> {/* 通过 props 传值给 Avatar */}</Panel>);
}// Header 仅依赖自身 props,不依赖外部变量
function Header({ name }) {return <h1>{name}</h1>;
}// Avatar 仅依赖自身 props,不依赖外部变量
function Avatar({ person }) {return (<imgclassName="avatar"src={getImageUrl(person)}alt={person.name}width={50}height={50}/>);
}
三、优化后组件的纯粹性验证
-
输入依赖唯一:
Profile仅依赖传入的props.person。Header仅依赖props.name,Avatar仅依赖props.person。所有组件的渲染逻辑都基于明确的props,无全局变量依赖。
-
无副作用:渲染过程中仅做数据传递和计算,不修改任何外部变量或对象,符合纯函数要求。
-
输入输出确定:只要传入的
person相同,Profile传递给子组件的props就相同,Header和Avatar返回的 JSX 也必然一致,输出完全可预测。
四、关键结论
React 组件间的数据传递应遵循“单向数据流” 原则:父组件通过 props 向子组件传递数据,子组件仅通过 props 接收数据。使用全局变量共享数据会破坏组件的独立性和纯粹性,导致逻辑混乱、难以维护。而基于 props 的通信能保证数据流向清晰,组件行为可预测,这也是 React 推荐的最佳实践。
如果需要在更深层级的组件间共享数据(避免 props 逐层传递),可以了解 React 的 Context API 或状态管理库(如 Redux),它们是更规范的跨组件数据共享方案。
