概述
React中的状态管理是其核心机制之一,它决定了组件的渲染和交互行为。以下是对React中状态管理工作原理的详细解释:
一、状态的定义与分类
在React中,状态(state)是组件记忆信息的一种方式,它决定了组件的渲染输出。状态可以是任何类型的数据,如数字、字符串、对象或数组等。根据状态的使用范围,可以将其分为本地状态和全局状态。
- 本地状态:本地状态是指仅在组件内部使用的状态,由组件自身维护和更新,不会被其他组件访问或修改。本地状态对于处理组件私有的数据非常有用。
- 全局状态:全局状态是指可以被多个组件共享和访问的状态,它通常用于存储跨组件的共享数据,如用户认证信息、主题设置等。
二、本地状态管理
本地状态管理主要通过React的useState
钩子函数实现。useState
是一个React Hook,它接受一个初始状态值,并返回一个包含当前状态值和一个用于更新状态的函数的数组。
- 初始化状态:当组件首次渲染时,
useState
会接受一个初始状态值,并将其作为组件的初始状态。 - 更新状态:当需要更新状态时,可以调用
useState
返回的更新函数,并传入新的状态值。React会重新渲染该组件,以反映状态的变化。
三、全局状态管理
全局状态管理在React中有多种实现方式,其中最常用的是Redux和React Context。
-
Redux:
- 核心概念:Redux是一个独立于React的状态管理库,它提供了store(存储状态)、action(描述状态变更)和reducer(处理状态变更)三个核心概念。
- 工作原理:通过定义一个全局的store来存储应用的状态,当需要更新状态时,通过dispatch函数发送一个action到reducer。Reducer是一个纯函数,它接受当前状态和action作为参数,并返回一个新的状态。然后,store会使用这个新状态来更新应用的状态树。
- 优势:Redux具有强大的中间件支持(如Redux Thunk、Redux Saga等),可用于处理异步操作、日志记录等。此外,Redux DevTools等工具可用于调试和查看状态变化。
-
React Context:
- 工作原理:React Context提供了一种在组件树中传递数据的方式,而无需在每一层组件中手动传递props。通过创建一个Context对象和一个Provider组件,可以将数据从顶层组件传递到下层组件。
- 使用场景:React Context适用于简单的全局状态共享场景,如主题切换、用户认证等。对于复杂的状态管理需求,建议使用Redux等更强大的工具。
四、状态更新的优化
在React中,状态更新是一个性能敏感的操作。为了优化性能,React使用了一些策略来减少不必要的重新渲染。
- 批处理更新:React会合并同一事件循环中的所有状态更新,并在事件处理结束后只进行一次重新渲染。
- 纯函数组件:使用函数组件和React Hooks可以更容易地实现性能优化。例如,使用
useMemo
和useCallback
等Hooks可以避免不必要的计算和渲染。 - 不可变数据结构:使用不可变数据结构可以减少状态比较的开销,并提高性能。
五、总结
React中的状态管理是一个复杂而强大的机制,它支持本地状态和全局状态的管理。通过理解状态的定义、分类以及管理方式,可以更有效地开发React应用。在实际开发中,可以根据具体的需求和项目特点来选择合适的状态管理方案。
State _ 如同一张快照
设置 state 只会为下一次渲染变更 state 的值。在第一次渲染期间,number
为 0
。这也就解释了为什么在 那次渲染中的 onClick
处理函数中,即便在调用了 setNumber(number + 1)
之后,number
的值也仍然是 0
:
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 1);setNumber(number + 1);setNumber(number + 1);}}>+3</button></>)
}
类似!一个 state 变量的值永远不会在一次渲染的内部发生变化
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 5);alert(number);}}>+5</button></>)
}//结果
//setNumber(0 + 5);
//alert(0);import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 5);setTimeout(() => {alert(number);}, 3000);}}>+5</button></>)
}//结果
//setNumber(0 + 5);
//setTimeout(() => {
// alert(0);
//}, 3000);
State_状态更新函数
在下次渲染前多次更新同一个 state
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(n => n + 1);setNumber(n => n + 1);setNumber(n => n + 1);}}>+3</button></>)
}
//每次+3
下面是 React 在执行事件处理函数时处理这几行代码的过程:
setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将它加入队列。setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将它加入队列。setNumber(n => n + 1)
:n => n + 1
是一个函数。React 将它加入队列。
当你在下次渲染期间调用 useState
时,React 会遍历队列。之前的 number
state 的值是 0
,所以这就是 React 作为参数 n
传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为 n
传递给下一个更新函数,以此类推:
更新队列 | n | 返回值 |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React 会保存 3
为最终结果并从 useState
中返回。
这就是为什么在上面的示例中点击“+3”正确地将值增加“+3”。
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);setNumber(42);}}>增加数字</button></>)
}
以下是 React 在执行事件处理函数时处理这几行代码的过程:
setNumber(number + 5)
:number
为0
,所以setNumber(0 + 5)
。React 将 “替换为5
” 添加到其队列中。setNumber(n => n + 1)
:n => n + 1
是一个更新函数。React 将该函数添加到其队列中。setNumber(42)
:React 将 “替换为42
” 添加到其队列中。
在下一次渲染期间,React 会遍历 state 队列:
更新队列 | n | 返回值 |
---|---|---|
“替换为 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“替换为 42 ” | 6 (未使用) | 42 |
然后 React 会保存 42
为最终结果并从 useState
中返回。
总而言之,以下是你可以考虑传递给 setNumber
state 设置函数的内容:
- 一个更新函数(例如:
n => n + 1
)会被添加到队列中。 - 任何其他的值(例如:数字
5
)会导致“替换为5
”被添加到队列中,已经在队列中的内容会被忽略。
事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此 更新函数必须是 纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。
更新State对象
更新嵌套对象※
setPerson({...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!}
});
Immer库
- 运行
npm install use-immer
添加 Immer 依赖 - 用
import { useImmer } from 'use-immer'
替换掉import { useState } from 'react'
const [person, updatePerson] = useImmer({name: 'Niki de Saint Phalle',artwork: {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',}});function handleNameChange(e) {updatePerson(draft => {draft.name = e.target.value;});}function handleTitleChange(e) {updatePerson(draft => {draft.artwork.title = e.target.value;});}function handleCityChange(e) {updatePerson(draft => {draft.artwork.city = e.target.value;});}function handleImageChange(e) {updatePerson(draft => {draft.artwork.image = e.target.value;});}
更新State中数组
避免改变原数组,推荐使用返回新数组的
//替换push改变原数组
setArtists( // 替换 state[ // 是通过传入一个新数组实现的...artists, // 新数组包含原数组的所有元素{ id: nextId++, name: name } // 并在末尾添加了一个新的元素]
);//替换unshift
setArtists([{ id: nextId++, name: name },...artists // 将原数组中的元素放在末尾
]);//filter过滤不满足条件的(删除)
setArtists(artists.filter(a => a.id !== artist.id)
);
//数组中插入对象const insertAt = 1; // 可能是任何索引const nextArtists = [// 插入点之前的元素:...artists.slice(0, insertAt),// 新的元素:{ id: nextId++, name: name },// 插入点之后的元素:...artists.slice(insertAt)//翻转先拷贝,再拷贝基础上翻转const nextList = [...list];nextList.reverse();setList(nextList);//更新内部对象
setMyList(myList.map(artwork => {if (artwork.id === artworkId) {// 创建包含变更的*新*对象return { ...artwork, seen: nextSeen };} else {// 没有变更return artwork;}
}));
Immmer库
const [myList, updateMyList] = useImmer(initialList);const [yourList, updateYourList] = useImmer(initialList);function handleToggleMyList(id, nextSeen) {updateMyList(draft => {const artwork = draft.find(a =>a.id === id);artwork.seen = nextSeen;});}function handleToggleYourList(artworkId, nextSeen) {updateYourList(draft => {const artwork = draft.find(a =>a.id === artworkId);artwork.seen = nextSeen;});}