1 修改组件纯粹的练习
javascript
export default function StoryTray({ stories }) {
stories.push({
id: 'create',
label: 'Create Story'
});
return (
<ul>
{stories.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
这个 StoryTray 组件不是纯粹的 ,核心问题在于它直接修改了传入的 props.stories 数组,违反了 React 中 "props 是只读的" 原则,会产生不可预测的副作用。
一、组件不纯粹的关键原因
1. 直接修改 props,产生副作用
stories.push(...) 这行代码是在直接修改父组件传入的 props.stories 数组 (push 方法会改变原数组)。React 明确规定:props 是只读的,组件绝不能修改自身接收的 props。原因是:
props本质上是父组件传递给子组件的数据,子组件修改props会导致 "父组件状态与子组件操作不一致"(比如父组件不知道子组件偷偷改了数据)。- 若父组件因其他原因重新渲染,传入的
stories可能被重置,导致子组件的修改丢失;反之,子组件的修改也可能意外影响父组件的其他逻辑(比如父组件中其他使用stories的地方)。
2. 同输入可能不同输出,违反确定性
假设父组件传入的 stories 是 [{ id: '1', label: 'Story 1' }]:
- 第一次渲染时,
stories.push(...)会给原数组增加一个元素,组件返回包含 2 个元素的列表。 - 若父组件重新渲染(比如其他状态变化),再次传入相同的初始
stories,push会再次执行,数组会变成 3 个元素,导致输出结果不同。
这种 "相同输入却产生不同输出" 的情况,完全违背了纯函数的确定性原则。
二、优化方案:创建新数组,不修改原 props
解决办法是基于原 props.stories 创建一个新数组 ,在新数组上添加元素,避免修改原 props。优化后的代码如下:
javascript
export default function StoryTray({ stories }) {
// 创建新数组:复制原 stories 中的元素,再添加新元素(不修改原 props)
const allStories = [
...stories, // 复制原数组的所有元素
{ id: 'create', label: 'Create Story' } // 添加新元素
];
return (
<ul>
{allStories.map(story => ( // 使用新数组渲染
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
三、优化后组件的纯粹性验证
-
无副作用 :不再修改原
props.stories,而是通过扩展运算符(...)复制原数组并创建新数组,保证了props的只读性,没有影响外部状态。 -
输入输出确定 :只要传入的
stories相同,allStories的计算结果就完全相同,渲染的列表也必然一致,符合 "相同输入 → 相同输出" 的纯函数特性。 -
数据流向清晰 :子组件仅使用父组件传入的
props,不修改它,避免了父子组件状态冲突,逻辑可预测。
四、关键结论
React 中,props 的只读性是保证组件纯粹性和数据流清晰的核心原则。修改 props 就像 "偷偷修改别人借给你的东西",会导致整个应用状态混乱。正确的做法是:对于需要基于 props 加工的数据,始终创建新的副本(如新数组、新对象),而非直接修改原 props。这一点在处理数组和对象类型的 props 时尤其重要(因为它们是引用类型,修改会影响原数据)。
2 Javascript的push方法
Array.prototype.push() - JavaScript | MDN
push 是 JavaScript 中数组(Array)的内置方法 ,用于向数组的末尾添加一个或多个元素,并且会直接修改原数组(这也是它在 React 组件中容易引发问题的原因)。
一、push 方法的基础用法
语法:数组.push(元素1, 元素2, ...)作用:给原数组添加新元素,返回值是添加后数组的长度。
示例:
javascript
const fruits = ['苹果', '香蕉'];
// 向数组末尾添加元素
const newLength = fruits.push('橙子', '葡萄');
console.log(fruits); // 输出:['苹果', '香蕉', '橙子', '葡萄'](原数组被修改了)
console.log(newLength); // 输出:4(添加后数组的长度)
二、为什么在 React 组件中用 push 会出问题?
因为 push 会直接修改原数组 (产生 "副作用"),而 React 要求组件接收的 props 必须是 "只读的"(不能被修改)【非常形象了!!!!!】。
在你之前的 StoryTray 组件中:
javascript
// 传入的 props.stories 是一个数组
stories.push(/* 新元素 */); // 直接修改了原数组,违反了 props 只读原则
这会导致父组件中的原数组被意外修改,引发数据不一致的问题(比如父组件再次使用这个数组时,内容已经被子组件篡改了)。
三、React 中正确处理数组的方式
如果需要基于原数组添加元素,应该创建新数组(不修改原数组),常用方法有:
-
扩展运算符(
...)(推荐):javascript// 基于原数组创建新数组,并添加新元素 const newArray = [...原数组, 新元素1, 新元素2]; -
concat方法:javascript// concat 会返回新数组,原数组不变 const newArray = 原数组.concat(新元素1, 新元素2);
举例(对应之前的组件优化):
javascript
// 原数组:stories
// 正确方式:创建新数组,不修改原数组
const allStories = [...stories, { id: 'create', label: 'Create Story' }];
总结
push 是 JavaScript 数组的原生方法,功能是 "给原数组添加元素并修改它"。但在 React 中,由于 props 必须只读,直接对 props 中的数组使用 push 会导致副作用,破坏组件纯粹性。正确的做法是通过扩展运算符或 concat 等方式创建新数组,既满足需求又不修改原数据,这也是 React 中处理数组的最佳实践。
3 渲染树中的 HTML 标签在哪里

UIView | Apple Developer Documentation
https://learn.microsoft.com/en-us/dotnet/api/system.windows.frameworkelement?view=windowsdesktop-7.0
4 React摘要

一、渲染树
- 基本定义 树结构常用于表示实体间关系,在 UI 建模中很常见。React 的渲染树专门表示单次渲染过程中 React 组件之间的嵌套关系 ,比如一个页面由
Header组件、Content组件和Footer组件组成,这三个组件在渲染树中就呈现出以页面根组件为父,它们为子的嵌套结构。 - 动态变化性 由于 React 的条件渲染 机制,渲染树在不同渲染过程中可能发生变化。例如一个组件
ConditionalComponent,当属性isShow为true时渲染ChildA组件,为false时渲染ChildB组件,那么在不同属性值下,渲染树的子组件结构就会不同。 - 性能调试价值 渲染树能帮助识别顶级组件 和叶子组件 :
- 顶级组件:处于渲染树上层的组件,其渲染性能会影响其下所有子组件的渲染。比如一个负责数据获取和全局状态管理的顶级组件,如果其渲染逻辑过于复杂,会导致整个页面的渲染性能下降。
- 叶子组件:处于渲染树末端的组件,通常会频繁重新渲染(比如展示实时数据的组件)。识别这两类组件,能帮助开发者理解和调试渲染性能问题,比如对频繁渲染的叶子组件做性能优化(如使用
React.memo),对影响范围大的顶级组件优化其渲染逻辑。
二、依赖树
- 基本定义 依赖树表示React 应用程序中的模块依赖关系 ,即一个模块(或组件)依赖于哪些其他模块。例如一个
UserList组件依赖于UserService模块来获取用户数据,UserService又依赖于HttpUtil模块来发送网络请求,这些依赖关系就构成了依赖树。 - 构建工具的作用 构建工具(如 Webpack、Vite 等)会利用依赖树来捆绑必要的代码以部署应用程序。它会分析依赖树,找出哪些模块是应用运行所必需的,然后将这些模块的代码打包成一个或多个文件,避免打包冗余代码。
- 性能优化价值 依赖树有助于调试大型捆绑包带来的渲染速度过慢问题,还能发现哪些捆绑代码可以被优化。比如通过依赖树发现某个未使用的第三方库被错误地打包进了应用,就可以将其从依赖中移除,减小捆绑包体积,提升渲染速度;或者发现某些模块被多个地方重复依赖,可通过代码分割等方式优化。