- React 更新 state 时为什么要使用 Immutable 语法?
- Immutable 可持久化数据结构 是什么?
- 如何在 React 项目中使用相关 Immutable 类库?
从 useState 说起
状态更新流程
📢 setState(value)
React 内部流程:
Diffing render => VDom 协调 Reconciliation 重新渲染 状态更新请求 状态合并 提交 Commit
- 状态更新请求 :调用
setState
方法发送状态更新的请求; - 状态合并:React 内部会对状态进行合并,处理多个更新请求;
- 重新渲染 :根据合并后的状态,重新调用组件的
render
方法生成新的虚拟 DOM; - 协调(Reconciliation):React 会比较新旧虚拟 DOM 树,找出差异 Diffing;
- 提交(Commit):将差异应用到实际的 DOM 上,并触发相关生命周期钩子。
🚃 React 的核心目标:尽可能地避免实际 DOM 的更新,因为直接操作 DOM 是昂贵的。 |
---|
🚎 Diff 算法的目的:识别最小的更新集合,以最高效的方式更新实际的 DOM。 |
---|
如果提供的新值与当前 state
相同(由 Object.is
比较确定),React 将 跳过重新渲染 Render
该组件及其子组件。
📡 state 的更新是触发渲染的关键一步!
当然,这也是命中验证缓存命中的关键判断逻辑。
更新 state
🌴 数字、字符串和布尔值,这些类型的值在 JavaScript 中是不可变(immutable)的,通过替换它们的值以触发一次重新渲染。
javascript
setTitle('abc')
🌴 对象、数组等,这些类型的值是可变(mutable)的,需要创建新对象。
React 中更新 state 的数据(如:对象),不应该直接修改存放在 React state 中的对象。
javascript
obj1 = {a: 1}
obj2 = obj1
obj2.a = 2
Object.is(obj1, obj2) // true (直接跳过渲染,与预期不符!)
可以使用 Object.assign
、展开运算符...
、深拷贝、concat/slice
等方式来创建新的 state。
javascript
setData(prevData => {
return {
...prevData,
y: 6
}
})
⚠️ 想要更新一个对象时,需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。
不推荐直接修改 state
⚓ 以下来自 React 官方网站^1^:
- 调试 :如果你使用
console.log
并且不直接修改 state,你之前日志中的 state 的值就不会被新的 state 变化所影响。这样你就可以清楚地看到两次渲染之间 state 的值发生了什么变化 - 优化 :React 常见的 优化策略 依赖于如果之前的 props 或者 state 的值和下一次相同就跳过渲染。如果你从未直接修改 state ,那么你就可以很快看到 state 是否发生了变化。如果
prevObj === obj
,那么你就可以肯定这个对象内部并没有发生改变。 - 新功能 :我们正在构建的 React 的新功能依赖于 state 被 像快照一样看待 的理念。如果你直接修改 state 的历史版本,可能会影响你使用这些新功能。
- 需求变更:有些应用功能在不出现任何修改的情况下会更容易实现,比如实现撤销/恢复、展示修改历史,或是允许用户把表单重置成某个之前的值。这是因为你可以把 state 之前的拷贝保存到内存中,并适时对其进行再次使用。如果一开始就用了直接修改 state 的方式,那么后面要实现这样的功能就会变得非常困难。
- 更简单的实现:React 并不依赖于 mutation ,所以你不需要对对象进行任何特殊操作。它不需要像很多"响应式"的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来,或者在初始化时做其他工作。这也是为什么 React 允许你把任何对象存放在 state 中------不管对象有多大------而不会造成有任何额外的性能或正确性问题的原因。
小结
React 通过浅比较旧的和新的 prop:也就是说,它会考虑每个新的 prop 是否与旧 prop 引用相等。
- 如果每次父组件重新渲染时创建一个新的对象或数组,即使它们每个元素都相同,React 仍会认为它已更改。
- 如果在渲染父组件时创建一个新的函数,即使该函数具有相同的定义,React 也会认为它已更改。
👉 这在 性能:React 实战优化技巧 中有提及相关方案,如 React.memo
、useMemo
、useCallback
等。
React 性能优化点主要在 ① 减少 DOM 的渲染频次;② 减少 DOM 的渲染范围;③ 非必要的内容延后处理。
🌑 🌒 🌓 🌔 因此,不可变性尤为重要! 🌕 🌖 🌗 🌘
React 使用一种称为虚拟 DOM 的内部机制来维护用户界面的表示。当组件的属性(props)或状态(state)发生变化时,React 会更新虚拟 DOM 以反映这些变化。然后,React 通过比较新旧虚拟 DOM 来执行协调过程,从而决定实际 DOM 中哪些部分需要更新。
这种机制确保只有实际发生变化的部分才会被重新渲染,从而提高性能。然而,有时即使某些 DOM 元素本身没有变化,它们也可能因为其他部分的更改而被迫重新渲染,这是变化部分的副作用。
🚃 Diff 算法的基本原理:在 React 中,每次状态更新时,会生成一个新的虚拟 DOM 树,然后与旧的虚拟 DOM 树进行比较,找出两者之间的差异,这个过程称为"Diff 算法"。 |
---|
🚎 Diff 算法的复杂性:Diff 算法本质上是一个复杂的过程,尤其是在涉及复杂组件和数据结构时。理想情况下,Diff 算法应该能够快速地识别哪些部分是相同的,哪些部分需要更新。然而,当数据结构可变时,Diff 算法可能需要进行深度遍历,以确定哪些节点或属性发生了变化。 |
---|
如果组件的属性和状态是不可变的数据结构,可以显著简化 Diff 算法的复杂性。这是因为 Immutable 数据结构的不可变性特性:
🎶 引用比较 :由于每次更新都会产生一个完全新的对象,而不是修改原有对象,React 可以简单地通过比较对象的引用,来确定数据是否发生了变化。如果两个对象的引用不同,那么它们肯定不同,这比深度比较数据结构的每个部分要快得多;
🎵结构共享 :Immutable 数据结构在进行更新时,只创建变动部分的新实例,而保留未变部分的引用。这意味着在 Diff 算法中,React 能够迅速识别出哪些部分是完全相同的,从而跳过这些部分的比较,直接关注那些确实发生了变化的部分。
可持久化数据结构
可持久化数据结构,是一种能够在修改之后其保留历史版本(即可以在保留原来数据的基础上进行修改------比如增添、删除、赋值)的数据结构。这种数据结构实际上是不可变对象,因为相关操作不会直接修改被保存的数据,而是会在原版本上产生一个新分支。
- 完全可持久化数据结构:所有版本都可以被查询或修改;
- 部分可持久化数据结构:所有历史版本都可以被访问,但只有当前版本可以被修改。
可持久化数据结构的实现通常依赖于两个核心概念:版本控制和共享子结构。
- 版本控制:每当对数据结构进行修改时,都会创建一个新的版本,并保持对旧版本的引用。这样,旧版本的信息得以保留,用户可以通过访问这些版本来追溯历史状态。
- 共享子结构:在生成新版本时,可持久化数据结构会重用旧版本中未发生变化的部分,即共享子结构。这样可以减少空间复杂度,因为不需要为每个版本单独存储所有数据。
Immutable
Immutable 实现原理是 可持久化数据结构。
使用旧数据创建新数据时,要保证旧数据同时可用且不可变,同时为了避免 deepcopy 的性能损耗,其使用了 结构共享。
javascript
import update from 'immutability-helper';
let data = {
a: { msg: 1 },
b: { msg: 2 },
};
const newData = update(data, { b: { msg: { $set: 3 } } });
console.log(data === newData); // false
console.log(data.a === newData.a); // true
console.log(data.b === newData.b); // false
在 React 项目中使用
typescript
export default function Page() {
const [data, setData] = useState({a: {b: {c: [1, 2]}}});
/*
* 希望给 c 追加 3:[1, 2] => [1, 2, 3]
*/
}
① React 原生写法
typescript
setData(prevData => ({
...prevData, // 展开现有的data对象
a: {
...prevData.a, // 展开data.a
b: {
...prevData.a.b, // 展开data.a.b
c: [...prevData.a.b.c, 3] // 展开data.a.b.c并追加3
}
}
}));
② immutability-helper
typescript
import update from 'immutability-helper';
setData(prevData =>
update(prevData, {
a: {
b: {
c: { $push: [3] },
}
}
})
);
- https://react.docschina.org/learn/updating-objects-in-state#why-is-mutating-state-not-recommended-in-react 为什么在 React 中不推荐直接修改 state ↩︎