文章目录
- [react setState函数](#react setState函数)
-
- setState的参数是下一个状态state
- setState的参数是更新函数function
- setState异步与同步
-
- 合成事件
- [setState 实现原理](#setState 实现原理)
react setState函数
const [state, setState] = useState(initialState)
useState
钩子函数向组件添加一个状态变量,该函数返回状态变量(只用于显示 )与设置状态变量的setState
函数。
useState
的使用细节
- 调用修改函数修改
state
变量的值(state值发生变化)会触发组件的重新渲染,直接修改state
变量不会触发组件的重新渲染。 - 组件重新渲染等同于函数重新执行,函数体的代码会再次调用。执行
useState
时会返回本次渲染的state
值。setState()
是由钩子函数useState()
生成的,useState()
会保证数组的每次渲染都会获取到相同的setState()
React
只在初次渲染时保存初始状态,后续渲染将其忽略。如果useState
的参数是一个生成数据的函数,则可以直接将这个函数本身传递(仅在初始化期间调用一次),不是函数()
(这种情况每次渲染时调用此函数)
setState的参数是下一个状态state
- 在 React 中,状态被认为是只读的,因此应该替换它而不是改变现有对象。
- 当
state
值是一个对象时,setState(state)
修改的参数,由于是下一次渲染的state
值(组件重渲染后useState
返回state
值),需要使用新的对象去替换已有对象。
js
// user是对象,对象的地址没有发生变化,所以不会引起组件重新渲染
const [user, setUser] = useState({name:"ranran",age:18})
user.name = "xxx";
setUser(user);
// 解决方案:将其拷贝给另一个新对象,修改新对象的属性
setUser({...user,name:"xxx"}) // 后面的name会覆盖前面的name
setState()
去修改一个state
时,并不表示修改当前的state
,修改的是组件下一次渲染的state
(下一次 渲染中useState
返回的内容)
说明案例1
假设name
的初始值是Taylor
,点击按钮后触发handleClick
函数,修改name
的值,打印发现是修改前的值Taylor
。
js
// 点击按钮修改名字
const handleClick = ()=> {
setName('Robin'); // 下一次渲染中useState返回的内容
console.log(name); // Still "Taylor"! 本次渲染useState返回的内容
}
说明案例2
假设 age
为 42
,这个处理函数三次调用 setAge(age + 1)
,点击一次后,age
将只会变为 43
。
js
const handleClick() => {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
setState的参数是更新函数function
更新函数的参数是待定状态
的state
。
js
const handleClick = () => {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
React
将更新函数放入 队列 中。然后,在下一次渲染期间
(组件重渲染,useState
返回state
的过程中),它将按照相同的顺序 调用:
1.a => a + 1
将接收 42 作为待定状态,并返回 43 作为下一个状态。
2.a => a + 1
将接收 43 作为待定状态,并返回 44 作为下一个状态。
3.a => a + 1
将接收 44 作为待定状态,并返回 45 作为下一个状态。
按照惯例,通常将待定状态参数命名为状态变量名称的第一个字母,如 age 为 a。然而,你也可以把它命名为 prevAge 或者其他你觉得更清楚的名称。
批量更新
多个顺序的setState
不是同步地一个一个执行,会一个一个加入队列,然后最后一起执行 。在异步setState
更新队列时,存储的是合并状态(上述案例age的最终状态45)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新。
setState异步与同步
明确这里所说的同步和异步指的是 API 调用后更新 DOM 是同步还是异步的。
react18
之后已经全部异步了
设计为异步更新的原因
setState
设计为异步,可以显著的提升性能:如果每次调用setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新 (这也为什么在react可控范围类,希望setState
是异步的)。- 如果同步更新了
state
,但是还没有执行render
函数,而且props
依赖于state
中的数据,那么state
和props
不能保持同步;state
和props
不能保持一致性,会在开发中产生很多的问题。
setState是同步还是异步呢?
- 在React内部机制能检测到的地方,组件生命周期 (除
componentDidUpdate
) 或React合成事件 中,setState
是异步。 - 如果
setState
在原生JavaScript
控制的范围被调用,setState
是同步。
setState
的"异步"并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的"异步"。
合成事件
在 React
中为元素添加的事件被叫做合成事件(区分一下js的原生事件)
合成事件的好处
- 屏蔽了浏览器之间关于事件处理的兼容性问题,为合成事件对象内部提供了统一的 API。
- 提升性能,
React
并不会将事件添加到真正的DOM
元素上,事件都被委托给document
。
步骤说明
1.React
会在拥有事件的 DOM
对象身上添加一个 store
对象,在 store
对象中存储事件名称及事件处理函数,然后通过document
分发事件。
2.当事件被触发后,通过获取事件源对象,查看事件源对象中是否存在store
对象,获取 store
对象中事件处理函数,执行事件处理函数。
核心实现
react 中所有的合成事件都会经过dispatchEventForLegacyPluginEventSystem()
处理,其中通过设置全局变量isBatchingEventUpdates
来标志当前的变化是否发生在React
的可调度范围内。
js
/* 所有的事件都将经过此函数统一处理 */
function dispatchEventForLegacyPluginEventSystem(){
// handleTopLevel 事件处理函数 batchedEventUpdates 批量更新
batchedEventUpdates(handleTopLevel, bookKeeping);
}
function batchedEventUpdates(fn,a){
/* 开启批量更新 */
isBatchingEventUpdates = true;
try {
/* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */
return batchedEventUpdatesImpl(fn, a, b);
} finally {
/* 完成一次事件,批量更新 */
isBatchingEventUpdates = false;
}
}
setState 实现原理
- 当 setState 方法被调用后,将状态传递给组件更新器,让组件更新器将状态临时存储起来。每个组件都会有自己的组件更新器,当需要更新组件时调用组件更新器。
- 状态临时保存完成后判断当前是否为批量更新模式 ,如果是,将组件更新器添加到更新队列中;如果不是,直接更新组件。
当触发合成事件时, 在事件处理函数执行之前,会先将批量更新模式设置为true
,然后执行事件处理函数收集状态。当事件处理函数执行完成后,执行批量更新操作(从更新队列中获取组件更新器并调用)。组件更新器调用完成后再将批量更新模式设置为false
。 - 更新组件时,先判断是否有状态需要更新,如果有就先计算最新状态,将得出的最新状态重新设置给组件(
useState
的返回值?)。
计算状态时,如果状态是函数类型,调用函数传入当前状态,返回最新状态。如果状态是对象类型,使用对象状态覆盖原有状态。 - 组件状态计算完成后,通过调用组件内部的 render 方法获取新的 VirtualDOM,再通过 DOM 对象获取旧的虚拟 DOM,然后调用 diff 方法进行比对,对比完成后将差异更新到真实 DOM 对象中。