useState每次渲染时怎么执行?它怎么知道返回哪个state?state异步更新,setState的更新流程(状态队列)?本文记录了官方文档描述UI章节,与state相关的几个重点例子和官方解释。
state:组件的记忆
// 伪代码
const [index, setIndex] = useState(0);
...
click -> setIndex(index + 1)
以下是实际发生的情况:
- 组件进行第一次渲染。 因为你将
0
作为index
的初始值传递给useState
,它将返回[0, setIndex]
。 React 记住0
是最新的 state 值。 - 你更新了 state。当用户点击按钮时,它会调用
setIndex(index + 1)
。index
是0
,所以它是setIndex(1)
。这告诉 React 现在记住index
是1
并触发下一次渲染。 - 组件进行第二次渲染。React 仍然看到
useState(0)
,但是因为 React 记住 了你将index
设置为了1
,它将返回[1, setIndex]
。
React调用useState时怎么知道返回哪个state?
在同一组件的每次渲染中,Hooks 都依托于一个稳定的调用顺序。因为我们只在顶层调用Hooks,每次Hooks始终以相同的顺序被调用。
在 React 内部,为每个组件保存了一个数组,其中每一项都是一个 state 对。**它维护当前 state 对的索引值,在渲染之前将其设置为 “0”。每次调用 useState 时,React 都会为你提供一个 state 对并增加索引值。你可以在文章 React Hooks: not magic, just arrays中阅读有关此机制的更多信息。
let componentHooks = [];
let currentHookIndex = 0;
// useState 在 React 中是如何工作的(简化版)
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// 这不是第一次渲染
// 所以 state pair 已经存在
// 将其返回并为下一次 hook 的调用做准备
currentHookIndex++;
return pair;
}
// 这是我们第一次进行渲染
// 所以新建一个 state pair 然后存储它
pair = [initialState, setState];
function setState(nextState) {
// 当用户发起 state 的变更,
// 把新的值放入 pair 中
pair[0] = nextState;
updateDOM();
}
// Store the pair for future renders
// and prepare for the next Hook call.
// 存储这个 pair 用于将来的渲染
// 并且为下一次 hook 的调用做准备
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。state 完全私有于声明它的组件。
state:组件的快照
作为一个组件的记忆,state 不同于在你的函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!设置 state 只会为 下一次 渲染变更 state 的值。
⭐例一:
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
以下是这个按钮的点击事件处理函数通知 React 要做的事情:
-
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
-
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
-
setNumber(number + 1)
:number
是0
所以setNumber(0 + 1)
。- React 准备在下一次渲染时将
number
更改为1
。
- React 准备在下一次渲染时将
尽管你调用了三次 setNumber(number + 1)
,但在 这次渲染的 事件处理函数中 number
会一直是 0
,所以你会三次将 state 设置成 1
。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的 number
等于 1
而不是 3
。
⭐例二:
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>
</>
)
} // alert的是0!!
如果使用替代法,就能看到被传入提示框的 state “快照”
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
一个 state 变量的值永远不会在一次渲染的内部发生变化,React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。 你无需担心代码运行时 state 是否发生了变化。
但是,万一你想在重新渲染之前读取最新的 state 怎么办?
在下次渲染前多次更新同一个 state(总数读取最新的state)
这是一个不常见的用例,但是如果你想在下次渲染之前多次更新同一个 state,你可以像 setNumber(n => n + 1)
这样传入一个根据队列中的前一个 state 计算下一个 state 的 函数,而不是像 setNumber(number + 1)
这样传入 下一个 state 值。这是一种告诉 React “用 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>
</>
)
}
在这里,n => n + 1
被称为 更新函数。当你将它传递给一个 state 设置函数时:
- React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。
- 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
下面是 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”。
例一:替换state后更新state
setNumber(number + 5);
setNumber(n => n + 1);
更新队列 | n | 返回值 |
---|---|---|
“替换为 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
setState(x)
实际上会像setState(n => x)
一样运行,只是没有使用n
!
例二:更新state后替换state
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
更新队列 | n | 返回值 |
---|---|---|
“替换为 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“替换为 42 ” | 6 (未使用) | 42 |
总而言之,以下是可以考虑传递给 setNumber
state 设置函数的内容:
- 一个更新函数(例如:
n => n + 1
)会被添加到队列中。 - 任何其他的值(例如:数字
5
)会导致“替换为5
”被添加到队列中,已经在队列中的内容会被忽略。
事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此 **更新函数必须是 **纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。
挑战:自己实现状态队列
export function getFinalState(baseState, queue) {
let finalState = baseState;
for (let update of queue) {
if (typeof update === 'function') {
// TODO: 调用更新函数
finalState = update(finalState)
} else {
// TODO: 替换 state
finalState = update
}
}
return finalState;
}
更新State中的数组
- 你可以把数组放入 state 中,但你不应该直接修改它。
- 不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。
- 你可以使用
[...arr, newItem]
这样的数组展开语法来向数组中添加元素。 - 你可以使用
filter()
和map()
来创建一个经过过滤或者变换的数组。 - 你可以使用 Immer 来保持代码简洁。
在没有mutation的前提下更新数组
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push ,unshift | concat ,[...arr] 展开语法(例子) |
删除元素 | pop ,shift ,splice | filter ,slice (例子) |
替换元素 | splice ,arr[i] = ... 赋值 | map (例子) |
排序 | reverse ,sort | 先将数组复制一份(例子) |