本文默认读者对 useState 有最为基本的了解,比如知道他的写法应当是怎样的,下面着重介绍部分重要的、在开发过程中会踩的坑和一些特性,最后动手实现一个最基本的 useState 代码
⭐️ 注意事项:
状态只在下次更新时异步变化,如果在设置状态后立即读取值还是老状态的
如果提供的新状态与当前状态相同 (使用 Object.is 比较),将跳过重新渲染组件以及子组件
React 是批量合并更新 state 的。多个状态更新操作会被放入一个队列中,然后在适当的时机进行合并和批量处理。这可以防止在单个事件期间多次重新渲染,一定会按照顺序。在极少数情况下,需要强制 React 提前更新界面,例如访问 DOM,可以使用 flushSync。
例子:
设置状态后立即读取状态
使用 setState 并不会改变已执行代码的当前状态 只会影响从下一次渲染开始返回的内容,想获取到变化后的值,可以选用 useEffect
function handleClick() {
setName(‘Robin’);
console.log(name); // Still “Taylor”!
}
基于上一次状态更新值
注意 setState 特性
function handleClick() {
// ❌
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
// ✅
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
更新状态中的对象和数组
应该替换它而不是修改它
React 中状态应该是只读的
// Don’t mutate an object in state like this:
form.firstName = ‘Taylor’;
// ✅ Replace state with a new object
setForm({
…form,
firstName: ‘Taylor’
});
避免重新创建初始状态
createInitialTodos 有效执行只有组件挂载时一次,但在之后组件历次re-render时都会被执行,这就造成了资源浪费 只需要传递初始化函数声明就行
function TodoList() { // ❌
const [todos, setTodos] = useState(createInitialTodos());
// …
function TodoList() { // ✅
const [todos, setTodos] = useState(createInitialTodos);
// …
其他问题:https://react.dev/reference/react/useState#troubleshooting
// 存储状态的数组
let state = []
// 存储更改状态方法的数组
let setters = []
// 用来记录状态和更改状态方法对应关系的下标
let stateIndex = 0
function createSetter(index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState(initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
// 采用闭包缓存每个state对应的setState
setters.push(createSetter(stateIndex))
const value = state[stateIndex]
const setter = setters[stateIndex]
// 每创建完一组都要+1,用来作为下一组状态的索引
stateIndex++
return [value, setter]
}
// 因为状态更改要刷新视图,因此这里用ReactDom.render方法来模拟更改状态后刷新视图的操作
function render() {
// 每次调用render都要重置stateIndex,否则对应的索引无限递增将无法正确匹配state和setState之间的关系
stateIndex = 0
ReactDom.render(<App />, document.getElementById('root'))
}