使用Hooks时的疑惑
Hooks的出现让我们对Function Component
逐步拥有了对标 Class Component
的特性,比如私有状态以及生命周期函数等。useState
与useReducer
这两个Hooks
让我们可以在Function Component
里使用到私有状态。而useState
其实相当于简化版的useReducer
,下面就将这两个放到一起简单介绍下。
1、useState使用示例
function PersionInfo ({initialAge,initialName} ) {
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
return (
<>
Age: {age}, Name: {name}
<button onClick ={() => setAge(age + 1)}>Growing up</button >
</>
);
}
useState
可以初始化一个私有状态,它会返回这个状态的最新值和一个用来更新状态的方法。而useReducer
则是针对更复杂的状态管理场景:
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 >
</>
);
}
useReducer
同样也是返回当前最新的状态,并返回一个用来更新数据的方法。
在使用这两个方法的时候也许我们会想过这样的问题:
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
问题1、React内部如何区分这两个状态
Function Component
不像Class Component
那样可以将私有状态挂载到类实例中并通过对应的key
来指向对应的状态,而且每次的页面刷新或者组件的重新渲染都会使得Function
重新执行一遍。所以在React
中必定有一种机制来区分这些Hooks
。
const [age, setAge] = useState(initialAge);
const [state, dispatch] = useReducer(reducer, initialState);
问题2、React是如何在每次重新渲染之后都能返回最新的状态
Class Component
因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component
由于本质就是一个函数,并且每次渲染都会重新执行。所以React
必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
源码中找出上面问题的答案
先来了解useState
以及useReducer
的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:
import React, { useState } from 'react' ;
在项目中我们通常会以这种方式来引入useState
方法,被我们引入的这个useState
方法是什么样子的呢?其实这个方法就在源码 packages/react/src/ReactHook.js
中。
import ReactCurrentDispatcher from './ReactCurrentDispatcher' ;
function resolveDispatcher () {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
export function useState (initialState ) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
从源码中可以看到,我们调用的其实是 ReactCurrentDispatcher.js
中的dispatcher.useState()
,那么我们继续前往ReactCurrentDispatcher.js
文件:
import type { Dispacther } from 'react-reconciler/src/ReactFiberHooks' ;
const ReactCurrentDispatcher = {
current : (null : null | Dispatcher),
};
export default ReactCurrentDispatcher;
好吧,它继续将我们带向 react-reconciler/src/ReactFiberHooks.js
这个文件。那么我们继续前往这个文件。
export type Dispatcher = {
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A ) => S,
initialArg : I,
init?: (I ) => S,
): [S, Dispatch<A>],
useEffect(
create: () => (() => void ) | void ,
deps : Array <mixed> | void | null ,
): void ,
}
兜兜转转我们终于清楚了React Hooks
的源码就放 react-reconciler/src/ReactFiberHooks.js
目录下面。在这里如上图所示我们可以看到有每个Hooks
的类型定义。同时我们也可以看到Hooks
的具体实现,大家可以多看看这个文件。首先我们注意到,我们大部分的Hooks
都有两个定义:
const HooksDispatcherOnMount: Dispatcher = {
useEffect : mountEffect,
useReducer : mountReducer,
useState : mountState,
};
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect : updateEffect,
useReducer : updateReducer,
useState : updateState,
};
从这里可以看出,我们的Hooks
在Mount
阶段和Update
阶段的逻辑是不一样的。在Mount
阶段和Update
阶段他们是两个不同的定义。我们先来看Mount
阶段的逻辑。在看之前我们先思考一些问题。React Hooks
需要在Mount
阶段做什么呢?就拿我们的useState
和useReducer
来说:
我们需要初始化状态,并返回修改状态的方法,这是最基本的。
我们要区分管理每个Hooks。
提供一个数据结构去存放更新逻辑,以便后续每次更新可以拿到最新的值。
先看下mountState的实现。
function mountState (initialState ) {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function' ) {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last : null ,
dispatch : null ,
lastRenderedReducer,
lastRenderedState,
});
const dispatch = (queue.dispatch = (dispatchAction.bind(
null ,
currentlyRenderingFiber,
queue,
)));
return [hook.memoizedState, dispatch];
}
区分管理Hooks
关于初始化状态并返回状态和更新状态的方法。这个没有问题,源码也很清晰利用initialState
来初始化状态,并且返回了状态和对应更新方法 return [hook.memoizedState, dispatch]
。那么我们来看看React
是如何区分不同的Hooks
的,这里我们可以从 mountState
里的 mountWorkInProgressHook
方法和Hook
的类型定义中找到答案。
export type Hook = {
memoizedState : any,
baseState : any,
baseUpdate : Update<any, any> | null ,
queue : UpdateQueue<any, any> | null ,
next : Hook | null ,
};
首先从Hook
的类型定义中就可以看到,React
对Hooks
的定义是链表。也就是说我们组件里使用到的Hooks
是通过链表来联系的,上一个Hooks
的next
指向下一个Hooks
。这些Hooks
节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount
阶段 Hooks
函数调用的 mountWorkInProgressHook
方法里:
function mountWorkInProgressHook (): Hook {
const hook: Hook = {
memoizedState : null ,
baseState : null ,
queue : null ,
baseUpdate : null ,
next : null ,
};
if (workInProgressHook === null ) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
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
方法的时候,就会形成一个新的updata
对象,添加到queue
链表上,而且这个是一个循环链表。可以看一下dispatchAction
方法的实现:
function dispatchAction (fiber,queue,action ) {
const update = {
action,
next : null ,
};
const last = queue.last;
if (last === null ) {
update.next = update;
} else {
const first = last.next;
if (first !== null ) {
update.next = first;
}
last.next = update;
}
queue.last = update;
scheduleWork();
}
也就是每次执行dispatchAction
方法,比如setAge
或setName
。就会创建一个保存着此次更新信息的update
对象,添加到更新链表queue
上。然后每个Hooks
节点就会有自己的一个queque
。比如假设我们执行了下面几个语句:
1
2
3
setAge(19 );
setAge(20 );
setAge(21 );
那么Hooks链表就会变成这样:
在Hooks
节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update
阶段可以通过这些更新获取到最新的值返回。这就是在第一次调用useState
或useReducer
之后,每次更新都能返回最新值的原因。再来看看mountReducer
,你会发现和mountState
几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState
其实就是阉割版的useReducer
。这里不详细介绍mountReducer
了。
function mountReducer (reducer, initialArg, init, ) {
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),
});
const dispatch = (queue.dispatch = (dispatchAction.bind(
null ,
currentlyRenderingFiber,
queue,
)));
return [hook.memoizedState, dispatch];
}
然后我们来看看update
阶段,也就是看一下我们的useState
或useReducer
是如何利用现有的信息,去返回最新的最正确的值的。先来看一下useState
在update
阶段的代码也就是updateState
:
function updateState (initialState ) {
return updateReducer(basicStateReducer, initialState);
}
可以看到,updateState
底层调用的其实就调用updateReducer
,因为我们调用useState
的时候,并不会传入reducer
,所以这里会默认传递一个basicStateReducer
进去。我们先看看这个basicStateReducer
:
function basicStateReducer (state, action ) {
return typeof action === 'function' ? action(state) : action;
}
在使用useState(action)
的时候,action
通常会是一个值,而不是一个方法。所以baseStateReducer
要做的其实就是将这个action
返回。来继续看一下updateReducer
的逻辑:
function updateReducer (reducer,initialArg,init ) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
const last = queue.last;
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
逐步计算出最新的状态。
useState/useReducer 小总结
React 如何管理区分Hooks?
React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中
useState和useReducer如何在每次渲染时,返回最新的值?
每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
为什么不能在条件语句等中使用Hooks?
链表!
如图所示,我们在mount
阶段调用了useState('A')
, useState('B')
, useState('C')
,如果我们将useState('B')
放在条件语句内执行,并且在update
阶段中因为不满足条件而没有执行的话,那么没法正确的重回Hooks
链表中获取信息。React也会报错。
Hooks链表位置
现在已经了解了React
通过链表来管理 Hooks
,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks
链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的FiberNode
。
如图所示,组件构建的Hooks
链表会挂载到FiberNode
节点的memoizedState
上面去。
useEffect
先看下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
。
mountEffect
function mountEffect ( create,deps, ) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl (fiberEffectTag, hookEffectTag, create, deps ) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = pushEffect(hookEffectTag, create, undefined , nextDeps);
}
function pushEffect (tag, create, destroy, deps ) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next : (null : any),
};
if (componentUpdateQueue === null ) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
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;
}
也就是在mount
阶段我们所有的effect
都以链表的形式被挂载到了fiberNode
上。然后在组件渲染完毕之后,React
就会执行updateQueue
中的所有方法。
updateEffect
function updateEffect (create,deps ) {
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl (fiberEffectTag, hookEffectTag, create, deps ) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined ;
if (currentHook !== null ) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null ) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(NoHookEffect, create, destroy, nextDeps);
return ;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
update
阶段和mount
阶段类似,只不过这次会考虑effect
的依赖deps
,如果此次更新effect
的依赖没有变化的话,就会被打上NoHookEffect
标签,最后会在commit
阶段跳过改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) {
const destroy = effect.destroy;
effect.destroy = undefined ;
if (destroy !== undefined ) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
useEffect 小总结
useEffect做了什么?
FiberNdoe
节点中又一个updateQueue
链表来存放所有的本次渲染需要执行的effect
。
mountEffect
阶段和updateEffect
阶段会把effect
挂载到updateQueue
上。
updateEffect
阶段,deps
没有改变的effect
会被打上NoHookEffect tag
,commit
阶段会跳过该Effect
。
到此为止,useState/useReducer/useEffect
源码也阅读完毕了,相信有了这些基础,剩下的Hooks的源码阅读不会成问题,最后放上完整图示: