-
Notifications
You must be signed in to change notification settings - Fork 0
Description
关于useState
Q1: useState 如果保存状态?
Q2: useState 如何更新状态?
hook如何保存数据
FunctionComponent 的 render 只是函数调用。那么在 render 内部调用是如何获取到对应的数据?
例如:
useState获取stateuseMemo获取缓存数据
A: 每个组件都有个对应的 fiber 节点,用于保存组件的信息
每次 FunctionComponent render时,全局变量 currentlyRenderingFiber 都会被赋值为该 FunctionComponent 对应的 fiber 节点
所以,hook 内部其实是从 currentlyRenderingFiber 中获取状态信息的
多个hook如何保存数据
多个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 的调用顺序不能改变, 每次 render 时都是从一条固定顺序的链表中获取 hook 对应数据的
useState执行流程
const [ a, setA ] = useState();setA(useState返回值的第二个参数) 是改变state的方法,在源码中被称为 dispatchAction, 每当调用 dispatchAction, 都会创建一个代表一次更新的对象 update:
例如:
function App() {
const [ count, setCount ] = useState(0);
function increment() {
setCount(count + 1);
}
return <p onClick={increment}>{count}</p>;
}调用 setCount(count + 1),会创建:
const update = {
// 更新的数据
action: 1,
// 指向下一个更新
next: null
// ...省略其他字段
};如果是多次调用 dispatchAction,例如:
function increment() {
// 产生update1
setCount(num + 1);
// 产生update2
setCount(num + 2);
// 产生update3
setCount(num + 3);
}那么,update 会形成一条环状链表。
既然这条 update 链表是由某个 useState 的 dispatchAction 产生,那么这条链表显然属于该 useState 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相同
下面我们看一个问题:
function App() {
const [ count, setCount ] = useState(0);
window.setCount = setCount;
return count;
}调用 window.setCount(1) 可以将视图中的 0 更新为 1 么?
我们需要看看这里的 setCount 方法的具体实现:
setCount === dispatchAction.bind(null, currentlyRenderingFiber, queue);可见,setCount 方法即绑定了 currentlyRenderingFiber 与 queue(即hook.queue) 的 dispatchAction。
上面已经介绍,调用 dispatchAction 的目的是生成 update,并插入到 hook.queue 链表中。
既然 queue 作为预置参数已经绑定给 dispatchAction,那么调用 dispatchAction 就步仅局限在 FunctionComponent 内部了。
update的action
function App() {
const [ count, setCount ] = useState(0);
function increment() {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}
return <p onClick={increment}>{count}</p>;
}上面的结果是?
调用 setCount 会产生 update,其中传参会成为 update.action。
在1秒内点击5次。在点击第五次时,第一次点击创建的 update 还没进入更新流程,所以 hook.baseState 还未改变。
那么这5次点击产生的 update 都是基于同一个 baseState 计算新的 state,并且 count 变量也还未变化(即5次update.action(即count + 1)为同一个值)。
所以,最终渲染的结果为1。
useState与useReducer
那么,如何5次点击让视图从1逐步变为5呢?
我们知道,需要改变 baseState 或者 action。
其中 baseState 由 React 的更新流程决定,我们无法控制。
但是我们可以控制 action。
action不仅可以传值,也可以传函数。
// action为值
setCount(count+ 1);
// action为函数
setCount(count=> count + 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;
}

