以下文章来源于魔术师卡颂 ,作者卡颂
魔术师卡颂
《React技术揭秘》作者,React Contributor
作为
React
开发者,你能答上如下两个问题么:
对于如下函数组件:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
调用
window.updateNum(1)
可以将视图中的
更新为
1
么?
对于如下函数组件:
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return
{num}
;}
在1秒内快速点击
p
5次,视图上显示为几?
向右滑动展示答案 1. 可以
2. 显示为1
其实,这两个问题本质上是在问:
useState如何保存状态?useState如何更新状态?本文会结合源码,讲透如上两个问题。
这些,就是你需要了解的关于
useState
的一切。
hook如何保存数据
FunctionComponent
的
render
本身只是函数调用。
那么在
render
内部调用的
hook
是如何获取到对应数据呢?
比如:
useState获取stateuseRef获取refuseMemo获取缓存的数据答案是:
每个组件有个对应的
fiber节点
(可以理解为
虚拟DOM
),用于保存组件相关信息。
每次
FunctionComponent
render
时,全局变量
currentlyRenderingFiber
都会被赋值为该
FunctionComponent
对应的
fiber节点
。
所以,
hook
内部其实是从
currentlyRenderingFiber
中获取状态信息的。
多个hook如何获取数据
我们知道,一个
FunctionComponent
中可能存在多个
hook
,比如:
function App() {
// hookA
const [a, updateA] = useState(0);
// hookB
const [b, updateB] = useState(0);
// hookC
const ref = useRef(0);
return
}
那么多个
hook
如何获取自己的数据呢?
答案是:
currentlyRenderingFiber.memoizedState
中保存一条
hook
对应数据的单向链表。
对于如上例子,可以理解为:
const hookA = {
// hook保存的数据
memoizedState: null,
// 指向下一个hook
next: hookB
// ...省略其他字段
};
hookB.next = hookC;
currentlyRenderingFiber.memoizedState = hookA;
当
FunctionComponent
render
时,每执行到一个
hook
,都会将指向
currentlyRenderingFiber.memoizedState
链表的指针向后移动一次,指向当前
hook
对应数据。
这也是为什么
React
要求
hook
的调用顺序不能改变(不能在条件语句中使用
hook
) —— 每次
render
时都是从一条固定顺序的链表中获取
hook
对应数据的。
useState执行流程
我们知道,
useState
返回值数组第二个参数为改变state的方法。
在源码中,他被称为
dispatchAction
。
每当调用
dispatchAction
,都会创建一个代表一次更新的对象
update
:
const update = {
// 更新的数据
action: action,
// 指向下一个更新
next: null
};
对于如下例子
function App() {
const [num, updateNum] = useState(0);
function increment() {
updateNum(num + 1);
}
return
{num}
;}
调用
updateNum(num + 1)
,会创建:
const update = {
// 更新的数据
action: 1,
// 指向下一个更新
next: null
// ...省略其他字段
};
如果是多次调用
dispatchAction
,例如:
function increment() {
// 产生update1
updateNum(num + 1);
// 产生update2
updateNum(num + 2);
// 产生update3
updateNum(num + 3);
}
那么,
update
会形成一条环状链表。
update3 --next--> update1
^ |
| update2
|______next_______|
这条链表保存在哪里呢?
既然这条
update
链表是由某个
useState
的
dispatchAction
产生,那么这条链表显然属于该
useState hook
。
我们继续补充
hook
的数据结构。
const hook = {
// hook保存的数据
memoizedState: null,
// 指向下一个hook
next: hookForB
// 本次更新以baseState为基础计算新的state
baseState: null,
// 本次更新开始时已有的update队列
baseQueue: null,
// 本次更新需要增加的update队列
queue: null,
};
其中,
queue
中保存了本次更新
update
的链表。
在计算
state
时,会将
queue
的环状链表剪开挂载在
baseQueue
最后面,
baseQueue
基于
baseState
计算新的
state
。
在计算
state
完成后,新的
state
会成为
memoizedState
。
为什么更新不基于memoizedState而是baseState,是因为state的计算过程需要考虑优先级,可能有些update优先级不够被跳过。所以memoizedState并不一定和baseState相同。更详细的解释见React技术揭秘[1]
回到我们开篇第一个问题:
function App() {
const [num, updateNum] = useState(0);
window.updateNum = updateNum;
return num;
}
调用
window.updateNum(1)
可以将视图中的
更新为
1
么?
我们需要看看这里的
updateNum
方法的具体实现:
updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);
可见,
updateNum
方法即绑定了
currentlyRenderingFiber
与
queue
(即
hook.queue
)的
dispatchAction
。
上文已经介绍,调用
dispatchAction
的目的是生成
update
,并插入到
hook.queue
链表中。
既然
queue
作为预置参数已经绑定给
dispatchAction
,那么调用
dispatchAction
就步仅局限在
FunctionComponent
内部了。
update的action
第二个问题
function App() {
const [num, updateNum] = useState(0);
function increment() {
setTimeout(() => {
updateNum(num + 1);
}, 1000);
}
return
{num}
;}
在1秒内快速点击
p
5次,视图上显示为几?
我们知道,调用
updateNum
会产生
update
,其中传参会成为
update.action
。
在1秒内点击5次。在点击第五次时,第一次点击创建的
update
还没进入更新流程,所以
hook.baseState
还未改变。
那么这5次点击产生的
update
都是基于同一个
baseState
计算新的
state
,并且
num
变量也还未变化(即5次
update.action
(即
num + 1
)为同一个值)。
所以,最终渲染的结果为1。
useState与useReducer
那么,如何5次点击让视图从1逐步变为5呢?
由以上知识我们知道,需要改变
baseState
或者
action
。
其中
baseState
由
React
的更新流程决定,我们无法控制。
但是我们可以控制
action
。
action
不仅可以传
值
,也可以传
函数
。
// action为值
updateNum(num + 1);
// action为函数
updateNum(num => num + 1);
在基于
baseState
与
update
链表生成新
state
的过程中:
let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;
// 遍历baseQueue中的每一个update
do {
if (typeof update.action === 'function') {
newState = update.action(newState);
} else {
newState = action;
}
} while (update !== firstUpdate)
可见,当传
值
时,由于我们5次
action
为同一个值,所以最终计算的
newState
也为同一个值。
而传
函数
时,
newState
基于
action
函数计算5次,则最终得到累加的结果。
如果这个例子中,我们使用
useReducer
而不是
useState
,由于
useReducer
的
action
始终为
函数
,所以不会遇到我们例子中的问题。
事实上,
useState
本身就是预置了如下
reducer
的
useReducer
。
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
总结
通过本文,我们了解了
useState
的完整执行过程。
本系列文章接下来会继续以
实例
+
源码
的方式,解读业务中经常使用的
React
特性。
参考资料
[1]
React技术揭秘: https://react.iamkasong.com/state/priority.html#%E4%BB%80%E4%B9%88%E6%98%AF%E4%BC%98%E5%85%88%E7%BA%A7