(一)实现一个简易的useState
整体数据结构
需求
function App() {
const [age, updateAge] = useState(0);
const [num,updateNum] = useState(5);
console.log(`${
isMount ? 'mount' : 'update'} age: `,age,'num',num)
return {
click() {
updateAge(num => num + 1);
updateAge(num => num + 8);
updateNum(num => num + 5);
}
}
}
我们观察以上代码,实现代码主要考虑:
1.调用ReactDOM.render会产生mount的更新,useState的返回的age,num是initialValue(即0和5)
2.点击click事件会产生update的更新,每次调用updateAge或者updateNum,去更新组件App,useState返回的num/age为更新后的结果
更新是什么
更新是如下数据结构:
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
update数据结构
这些update是如何组合在一起呢?
答案是:他们会形成环状单向链表。
调用updateNum,updateAge实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:
function dispatchAction(queue, action) {
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
if (queue.pending === null) {
update.next = update;
} else {
// 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
// 1.新的update的next指向第一个update
update.next = queue.pending.next;
// 2.最后一个update的next指向新的update
queue.pending.next = update;
}
// queue.pending始终指向最后一个插入的update
queue.pending = update;
schedule();
}
状态如何保存
更新产生的update对象会保存在对应的hook的queue中。对于函数式组件来说,hook存储在哪里呢?
FunctionComponent对应的fiber中。
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null,
// 指向App函数
stateNode: App
};
Hook数据结构
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null
},
// 保存hook对应的state
memoizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
整体思路:
1.mount的时候,在useState函数中创建hook,把这个hook利用workInProgressHook指针(指向上一个hook)和上一个hook串起来,形成hooks单向无环链表。返回 [initialState, dispatchAction.bind(null, hook.queue)]。
2.调用updateAge/updateNum的时候,即是在调用dispatchAction.bind(null, hook.queue),
将此次的update的对象和之前的update对象们组合成环状单向链表。并且模拟React开始调度更新。
调度更新中会执行useState函数,根据其对应的hook对象及其hooks环状单向链条,计算得出更新的结果baseState,返回 [baseState, dispatchAction.bind(null, hook.queue)];
整体代码
let workInProgressHook;
// 通过workInProgressHook变量指向当前正在工作的hook。
let isMount = true;
// 首次render时是mount
const fiber = {
memoizedState: null,
stateNode: App
};
function schedule() {
// 更新前将workInProgressHook重置为fiber保存的第一个Hook
workInProgressHook = fiber.memoizedState;
// 触发组件render
const app = fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
return app;
}
// 1.创建update
// 2.环状单向链表操作
// 3.模拟React开始调度更新
function dispatchAction(queue, action) {
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
if (queue.pending === null) {
update.next = update;
} else {
// 环状单向链表: 第一个update的next指向第二个update,第二个update的next指向第三个update,....,最后一个update指向第一个update
// 1.新的update的next指向第一个update
update.next = queue.pending.next;
// 2.最后一个update的next指向新的update
queue.pending.next = update;
}
// queue.pending始终指向最后一个插入的update
queue.pending = update;
schedule();
}
function useState(initialState) {
let hook;
if (isMount) {
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while(firstUpdate !== hook.queue.pending.next)
}
hook.memoizedState = initialState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function App() {
const [age, updateAge] = useState(0);
const [num,updateNum] = useState(5);
console.log(`${
isMount ? 'mount' : 'update'} age: `,age,'num',num)
return {
click() {
updateAge(num => num + 1);
updateAge(num => num + 8);
updateNum(num => num + 5);
}
}
}
const myApp = schedule();
myApp.click()
(二)原理解析
源码中,我们大部分的Hooks都有两个定义:
// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useReducer: mountReducer,
useState: mountState,
// 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useReducer: updateReducer,
useState: updateState,
// 其他Hooks
};
从这里可以看出,我们的Hooks在Mount阶段和Update阶段的逻辑是不一样的。在Mount阶段和Update阶段他们是两个不同的定义。
1.useState
1.1 mountState
先来看mountState的实现。
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// 1.获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 2.声明一个链表来存放更新
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// 3.返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 4.返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
Hook的类型定义
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null, // 指向下一个Hook
};
这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里(workInProgressHook作为指针串联起来):
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上。比如我们的这个例子:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {
})
那么在mount阶段,就会生产如下图这样的单链表:
useState和useReducer都是使用了一个queue链表来存放每一次的更新。以便后面的update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的update对象,添加到queue链表上,而且这个是一个循环链表。可以看一下 dispatchAction 方法的实现:
// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action,) {
const update = {
action,
next: null,
};
// 将update对象添加到循环链表中
const last = queue.last;
if (last === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
last.next = update;
}
// 将表头保持在最新的update对象上
queue.last = update;
// 进行调度工作
scheduleWork();
}
也就是我们每次执行dispatchAction方法,比如setAge或setName。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。比如假设我们执行了下面几个语句:
setAge(19);
setAge(20);
setAge(21);
那么我们的Hooks链表就会变成这样:
在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。
而组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。
1.2 updateState
然后我们来看看update阶段,也就是看一下我们的useState或useReducer是如何利用现有的信息,去给我们返回最新的最正确的值的。先来看一下useState在update阶段的代码也就是updateState:
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
可以看到,updateState底层调用的其实是updateReducer,因为我们调用useState的时候,并不会传入reducer,所以这里会默认传递一个basicStateReducer进去。我们先看看这个basicStateReducer:
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回。来继续看一下updateReducer的逻辑:
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const last = queue.last;
// 获取最早的那个update对象
first = last !== null ? last.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
// 返回最新的状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
在update阶段,也就是我们组件第二次第三次。。执行到useState或useReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useState的reducer是baseStateReducer,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。
2.useReducer
const initialState = {
age: 0, name: 'Dan'};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state, age: state.age + action.age};
case 'decrement':
return {
...state, age: state.age - action.age};
default:
throw new Error();
}
}
function PersionInfo() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Age: {
state.age}, Name: {
state.name}
<button onClick={
() => dispatch({
type: 'decrement', age: 1})}>-</button>
<button onClick={
() => dispatch({
type: 'increment', age: 1})}>+</button>
</>
);
}
2.1 补充mountReducer
mountReducer,你会发现和mountState几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState其实就是阉割版的useReducer
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
let initialState;
// 初始化
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg ;
}
hook.memoizedState = hook.baseState = initialState;
// 存放更新对象的链表
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
3.总结
3.1 React 如何管理区分Hooks?
React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中
3.2 useState和useReducer如何在每次渲染时,返回最新的值?
每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
3.3 useState为什么不能放到条件语句里面?
这是因为React通过单链表来管理Hooks。
update 阶段,每次调用 useState,会根据其对应的hook对象及其update环状单向链条,计算得出更新的结果。
那么如何得到对应的hook对象呢,我们是用了一个指针workInProgressHook去顺着hooks链表依次获取的,每次调用 useState,链表就会执行 next 向后移动一步,workInProgressHook就会指向下一个useState对应的hook对象,在下一个useState中就可以直接使用进行计算了。
如果将 useState 写在条件判断中,假设条件判断不成立,没有执行里面的 useState 方法,会导致接下来所有的 useState 的hook对象的取值出现偏移,从而导致异常发生。
4.useEffect
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect(() =>{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect(() =>{
console.log(name)
}, [name])
return (
<>
...
</>
);
}
PersionInfo组件第一次渲染的时候会在控制台输出age和name,在后面组件的每次update中,如果useEffect中的deps依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在unmount的时候就会执行清除函数(如果有)
React中是怎么实现的呢?其实很简单,在FiberNode中通过一个updateQueue来存放所有的effect,然后在每次渲染之后依次执行所有需要执行的effect。useEffect 也分为mountEffect和updateEffect
4.1 mountEffect
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function mountEffect( create,deps,) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前Hook,并把当前Hook添加到Hook链表
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将当前effect保存到对应的Hook节点的memoizedState属性上,
// 以及添加到fiberNode的updateQueue上
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// componentUpdateQueue 会被挂载到fiberNode的updateQueue上
if (componentUpdateQueue === null) {
// 如果当前Queue为空,将当前effect作为第一个节点
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 保持循环
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 否则,添加到当前的Queue链表中
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
// componentUpdateQueue.lastEffect永远指向最后一个插入的effect
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
可以看到在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
也就是在mount阶段我们所有的effect都以链表的形式被挂载到了fiberNode上。然后在组件渲染完毕之后,React就会执行updateQueue中的所有方法。
4.2 updateEffect
update阶段和mount阶段类似,只不过这次会考虑effect 的依赖deps,如果此次更新effect的依赖没有变化的话,就会被打上NoHookEffect标签,最后会在commit阶段跳过该effect的执行。
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function updateEffect(create,deps){
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
// 获取当前Hook节点,并把它添加到Hook链表
const hook = updateWorkInProgressHook();
// 依赖
const nextDeps = deps === undefined ? null : deps;
// 清除函数
let destroy = undefined;
// 将当前effect保存到对应的Hook节点的memoizedState属性上,
// 以及添加到fiberNode的updateQueue上
if (currentHook !== null) {
// 拿到前一次渲染该Hook节点的effect
// hook对象上memoizedState存储了effect,effect.deps是前一次渲染时候的依赖
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比deps依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
// effect的执行
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
commit阶段跳过NoHookEffect的effect的执行。
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount 阶段执行所有tag !== NoHookEffect的effect.create,
// 我们的清除函数(如果有)会被返回给destroy属性,等到unmount的时候执行
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
5.总结useEffect做了什么?
1.FiberNdoe节点中会有一个updateQueue链表来存放所有的本次渲染需要执行的effect。
2.mountEffect阶段和updateEffect阶段会把effect 挂载到updateQueue上。
3.updateEffect阶段,deps没有改变的effect会被打上NoHookEffect tag,commit阶段会跳过该Effect。