useState更新相同的State,函数组件为什么执行2次?
这个问题实际很悬,大家可能平时没有注意到,引起我的注意的是掘金的一个掘友问我的一个问题,问题如下:

首先非常感谢这位细心的掘友的报案,我在
React-hooks 原理
中讲到过,对于更新组件的方法函数组件
useState
和类组件的
setState
有一定区别,
useState
源码中如果遇到两次相同的
state
,会默认阻止组件再更新,但是类组件中
setState
如果没有设置
PureComponent
,两次相同的
state
也会更新。
我们回顾一下
hooks
中是怎么样阻止组件更新的。
react-reconciler/src/ReactFiberHooks.js -> dispatchAction
if (is(eagerState, currentState)) {
return
}
scheduleUpdateOnFiber(fiber, expirationTime); // 调度更新
如果判断上一次的
state
->
currentState
,和这一次的
state
->
eagerState
相等,那么将直接
return
阻止组件进行
scheduleUpdate
调度更新。
所以我们想如果两次
useState
触发同样的state,那么组件只能更新一次才对,但是事实真的是这样吗?
。
顺着这位掘友提供的线索,我们开始写
demo
进行验证。
const Index = () => {
const [ number , setNumber ] = useState(0)
console.log('组件渲染',number)
return <div className="page" >
<div className="content" >
<span>{ number }</span><br/>
<button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
<button onClick={ () => setNumber(2) } >将number设置成2</button><br/>
<button onClick={ () => setNumber(3) } >将number设置成3</button>
</div>
</div>
}
export default class Home extends React.Component{
render(){
return <Index />
}
}
如上demo,三个按钮,我们期望连续点击每一个按钮,组件都会仅此渲染一
次,于是我们开始实验:
效果:

果然,我们通过
setNumber
改变
number
,每次连续点击按钮,组件都会更新2次,按照我们正常的理解,每次赋予
number
相同的值,只会渲染一次才对,但是为什么执行了2次呢?
可能刚开始会陷入困境,不知道怎么破案,但是我们在想
hooks
原理中讲过,每一个函数组件用对应的函数组件的
fiber
对象去保存
hooks
信息。
所以我们只能从
fiber
找到线索。
那么如何找到函数组件对应的fiber对象呢,这就顺着函数组件的父级
Home
入手了,因为我们可以从类组件
Home
中找到对应的fiber对象,然后根据
child
指针找到函数组件
Index
对应的
fiber
。
说干就干,我们将上述代码改造成如下的样子:
const Index = ({ consoleFiber }) => {
const [ number , setNumber ] = useState(0)
useEffect(()=>{
console.log(number)
consoleFiber() // 每次fiber更新后,打印 fiber 检测 fiber变化
})
return <div className="page" >
<div className="content" >
<span>{ number }</span><br/>
<button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
</div>
</div>
}
export default class Home extends React.Component{
consoleChildrenFiber(){
console.log(this._reactInternalFiber.child) /* 用来打印函数组件 Index 对应的fiber */
}
render(){
return <Index consoleFiber={ this.consoleChildrenFiber.bind(this) } />
}
}
我们重点关心fiber上这几个属性
Index fiber
上的
memoizedState
属性,
react hooks
原理文章中讲过,函数组件用
memoizedState
保存所有的
hooks
信息。
Index fiber
上的
alternate
属性
Index fiber
上的
alternate
属性上的
memoizedState
属性。
是不是很绕😂,马上会揭晓是什么。
Index
组件上的
useState
中的
number
。
首先我们讲一下
alternate
指针指的是什么?
说到
alternate
就要从
fiber
架构设计说起,每个
React
元素节点,用两颗fiber树保存状态,一颗树保存当前状态,一个树保存上一次的状态,两棵
fiber
树用
alternate
相互指向。
就是我们耳熟能详的
双缓冲
。
初始化打印
效果图:

初始化完成第一次render后,我们看一下fiber树上的这几个状态
第一次打印结果如下
fiber
上的
memoizedState
中
baseState = 0
即是初始化
useState
的值。
fiber
上的
alternate
为
null
。
Index
组件上的
number
为 0。
初始化流程:
首先对于组件第一次初始化,会调和渲染形成一个fiber树(我们
简称为树A
)。
树A的
alternate
属性为
null
。
第一次点击 setNumber(1)
我们第一次点击发现组件渲染了,然后我们打印结果如下:

树A上的
memoizedState
中 **
baseState = 0
。
树A上的
alternate
指向 另外一个
fiber
(我们这里称之为树B)。
Index
组件上的
number
为 1。
接下来我们打印树B上的
memoizedState

结果我们发现树B上
memoizedState
上的
baseState = 1
。
得出结论:
更新的状态都在树B上,而树A上的 baseState还是之前的0。
我们大胆猜测一下更新流程:
在第一次更新渲染的时候,由于树A中,不存在
alternate
,所以直接复制一份树A作为
workInProgress
(我们这里称之为
树B
)所有的更新都在当前树B中进行,所以 baseState 会被更新成 1,然后用当前的
树B
进行渲染。
结束后树A和树B通过
alternate
相互指向。
树B作为下一次操作的
current
树。
第二次点击 setNumber(1)
第二次打印,组件同样渲染了,然后我们打印fiber对象,效果如下:

fiber对象上的
memoizedState
中
baseState
更新成了 1。
然后我们打印一下
alternate
中
baseState
也更新成了 1。

第二次点击之后 ,树A和树B都更新到最新的 baseState = 1
首先我们分析一下流程:
当我们第二次点击时候,是通过上一次树A中的
baseState = 0
和
setNumber(1)
传入的 1做的比较。
所以发现
eagerState !== currentState
,组件又更新了一次。
接下来会以current树(树B)的
alternate
指向的树A作为新的
workInProgress
进行更新,此时的树A上的 baseState 终于更新成了 1 ,这就解释了为什么上述两个 baseState 都等于 1。
接下来组件渲染完成。
树A作为了新的 current 树。
在我们第二次打印,打印出来的实际是交替后树B,树A和树B就这样交替着作为最新状态用于渲染的
workInProgress
树和缓存上一次状态用于下一次渲染的
current
树。
第三次点击(三者言其多也)
那么第三次点击组件没有渲染,就很好解释了,第三次点击上一次树B中的
baseState = 1
和
setNumber(1)
相等,也就直接走了return逻辑。
通过这个容易忽略的案件,我们学习了双缓冲和更新机制。
双缓冲树:
React 用workInProgress树(内存中构建的树) 和
current
(渲染树) 来实现更新逻辑。
我们console.log打印的fiber都是在内存中即将
workInProgress的fiber树。
双缓存一个在内存中构建,在下一次渲染的时候,直接用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以防止只用一颗树更新状态的丢失的情况,又加快了
dom
节点的替换与更新。
current
workInProgress
dom
更新机制:
在一次更新中,首先会获取current树的alternate作为当前的 ,渲染完毕后,
workInProgressworkInProgress树变为current树。
我们用如上的树A和树B和已经保存的baseState模型,来更形象的解释了更新机制 。
hooks中的useState进行state对比,用的是缓存树上的state和当前最新的state。
所有就解释了为什么更新相同的state,函数组件执行2次了。
我们用一幅流程图来描述整个流程。

作者:我不是外星人
原文:https://juejin.cn/post/6963053793613185031
推荐阅读
“在看转发”是最大的支持
