使用 Hooks 时的疑惑
- React 如何管理区分Hooks?
- useState和useReducer如何在每次渲染时,返回最新的值?
- 为什么不能在条件语句等中使用Hooks?
React Hooks 源码入口
以 useState 为例,其他的等同
Mounted 阶段做了三步:
- 初始化状态,并返回修改状态的方法。
利用 initialState 来初始化状态,返回状态和对应更新方法:
return [hook.memoizedState, dispatch]
- 区分管理每个Hooks
通过mountWorkInProgressHook方法和Hook的类型定义
export type Hook = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 以及每次 dispatch 之后 newState
baseUpdate: Update | null,
queue: UpdateQueue | null,
next: Hook | null, // 指向下一个Hook,用来按顺序串联所有的 hook,可以看成一个 初始化为0 的cursor,每次进行+1
};
首先从Hook的类型定义中就可以看到,React 对Hooks的定义是链表。也就是说我们组件里使用到的Hooks是通过hash值链表来联系的,上一个Hooks的next指向下一个Hooks。这些Hooks节点通过mountWorkInProgressHook 方法将链表数据结构串联在一起,通过 next 按顺序串联所有的 hook。具体见代码
在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上
以一段代码为例
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})
那么在 mount 阶段,就会产生下面的链表图
- 返回最新的值
通过使用一个 queue 链表来存放每一次的更新。以便后面的 update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的updata对象,添加到queue链表上,而且这个是一个循环链表。
function dispatchAction() {
const update: Update = {
action,
next: (null: any),
};
// 将update对象添加到循环链表中
const pending = queue.pending;
if (pending === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
// 将 最新的 pending 赋值给 最新的 update
update.next = pending.next;
// 在最新的update对象后面插入新的update对象
pending.next = update;
}
// 确保queue.pending每次都是新的update对象
queue.pending = update;
// 进行调度工作
scheduleWork(fiber, expirationTime);
}
也就是我们每次执行dispatchAction方法,比如对上面的State age执行setAge。就会创建一个保存着这次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。
setAge(19);
setAge(20);
setAge(21);
我们的链表图就会变成
在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值(也就是我们的 queue.pending)返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。
update 阶段
function updateState(
initialState: (() => S) | S,
): [S, Dispatch>] {
return updateReducer(basicStateReducer, (initialState: any));
}
updateState 其实也是调用 updateReducer,而 updateReducer 是需要传入一个 reducer 的,但我们使用 setState 的时候并不会传入 reducer,所以这里会默认给我们传入一个 basicStateReducer
- basicStateReducer
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。
通过上面的链表图流程图我们对一开始的问题有了答案:
- React 如何管理区分Hooks?
- React通过单链表来管理Hooks
- 按Hooks的执行顺序依次将Hook节点添加到链表中
- useState和useReducer如何在每次渲染时,返回最新的值?
- 每个Hook节点通过循环链表记住所有的更新操作
- 在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
- 为什么不能在条件语句等中使用Hooks?
因为Hooks是通过单链表方式依次执行的,如果有一个条件不通过,那么我们的链表图就会对应不上,就会报错
比如:
[A, setA] = useState('A')
if (count > 0) {
[B, setB] = useState('B')
}
[C, setC] = useState('C')
我们在mount阶段调用了useState('A'), useState('B'), useState('C'),如果我们将useState('B') 放在条件语句内执行,并且在update阶段中因为不满足条件而没有执行的话,那么没法正确的重Hooks链表中获取信息
自己实现一个简易版的useState
let _state = [];
let index = 0;
const myUseState = num => {
// 之所以要把index赋给一个currentIndex变量是因为,
// 我们render完后紧接着要对index+1,这样就可以调用几次useState数组里面就有多少个state,
// 也就是index就会和state对应
const currentIndex = index;
_state[currentIndex] =
_state[currentIndex] === undefined ? num : _state[currentIndex];
const setN = num1 => {
_state[currentIndex] = num1;
render();
};
index += 1;
return [_state[currentIndex], setN];
};
function render() {
// 之所以render前先置为0是因为如果不置0就会一直累加下去,而页面根本没有那么多state
index = 0;
ReactDOM.render( , rootElement);
}
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
const x = () => {
setN(n + 1);
};
const y = () => {
setM(m + 1);
};
return (
{n}
{m}
);
}
我们的Hooks链表存在哪里?
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
从上面代码可以看出 我们每次的 hooks 都会先存到 workInProgressHook 然后存到 currentlyRenderingFiber.currentlyRenderingFiber 而 currentlyRenderingFiber 的类型是一个 Fiber节点
useEeffect
用来处理副作用,几个特点:
- 有两个参数 callback 和 dependencies 数组
- 如果 dependencies 不存在,那么 callback 每次 render 都会执行
- 如果 dependencies 存在,只有当它发生了变化, callback 才会执行
- 如果 dependencies 是空数组,那么只有在 Mounted 阶段才会执行
useEffect 也分为 mountEffect 和 updateEffect
mountEffect
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 添加到 memoizedState 链表作为最后一项
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag, // 一个二进制数,它将决定effect的行为
create, // 绘制后应该运行的回调
destroy, // 从create()返回的回调应该在初始渲染之前运行
deps, // 依赖项
next: (null: any), // 下一个effect 引用
};
// 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;
}
}
return effect;
}
在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
就是在mouted阶段effect和useState一样通过循环单链表的形式添加到一个updateQueue链表里,但是与useState不同的是它不会把最后一次的作为表头
比如:
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect(() =>{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect(() =>{
console.log(name)
}, [name])
return (
<>
...
>
);
}
updateEffect
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;
if (currentHook !== null) {
// 拿到前一次渲染该Hook节点的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比上一次和当前的依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖没有变化,就会传入一个NoHookEffect 作为 tag
// effect的执行
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
update 阶段和 mounted 阶段类似,只不过这里会判断更新 effect 的依赖有没有变化,如果没有就会传入一个 NoHookEffect
作为 tag,最后通过 tag 来判断是否执行 当前 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);
}
}
所以 useEffect 主要做了三步:
1). Fiber中会有一个updateQueue链表来存放所有的本次渲染需要执行的effect。
2). mountEffect阶段和updateEffect阶段会把effect push到updateQueue上。
3). updateEffect阶段,deps没有改变的effect会被打上NoHookEffect tag,commit阶段会跳过该Effect。
简易版 useEffect 实现
const memoizedState = []; // 存放每次传入的依赖
let cursor = 0;
function useEffect(callback, depArray) {
const noDeps = !depArray;
const deps = memoizedState[cursor];
// 依赖有变化
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
// 如果没有依赖或者依赖发生了变化一调用我们的callback
if (noDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
Hooks 和 Class 相比的优缺点?
优点:
- 代码可读性更高
- 代码量少
- 不用考虑
this
指向问题
缺点:
- 响应式的 useEffect
如果我们对依赖项定义的不准确的话,useEffect触发的次数会比我们预想的多; - 状态不同步
我们每次修改state都不会修改当前的state,而是会重新生成一个新的state,旧的state和新的state有可能同时存在,之后旧的state会被垃圾回收掉
function App() {
const [n, setN] = React.useState(0)
const log = () => {
setTimeout(() => console.log(`n: ${n}`), 3000)
}
return (
{n}
)
}
上面的代码有两种操作
1). 点击+1再点击log--> 得到的n的值就是当前的值+1
2). 点击log再点击+1--> 得到的值就是上一个n的值
在线代码运行:https://codesandbox.io/s/icy-firefly-imvfu
简易版 useState 原理:
https://www.jianshu.com/p/5c405a00ba06