【react hooks】--初渲染和更新阶段
hook组件初渲染
hooks
组件在初次渲染时,
- 解析组件类型,判断是
Function
还是class
类型,然后执行对应类型的处理方法 - 判断到当前是
Function
类型组件后,首先在当前组件,也就是fiberNode
上进行hook
的创建和挂载,将所有的hook api
都挂载到全局变量dispatcher
上 - 顺序执行当前组件,每遇到一个
hook api
都通过next
将它连接到当前fiberNode
的hook
链表上
hooks api 挂载
在初始渲染时,currentDispatcher
为空,就会先将所有hook api
挂载到当前fiberNode
的dispatcher
上。也就是将HooksDispatcherOnMountInDEV
赋值给dispatcher
{
// 首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}
而在HooksDispatcherOnMountInDEV
中,包含各种原生hook
,内部调用mountXXX
方法。
HooksDispatcherOnMountInDEV = {
useCallback: function (callback, deps) {
return mountCallback(callback, deps);
},
useEffect: function (create, deps) {
return mountEffect(create, deps);
},
useMemo: function (create, deps) {
return mountMemo(create, deps);
},
useState: function (initialState) {
return mountState(initialState);
}
}
useState -- mountState
- 首先创建一个
hook
节点,其中的memoizedState
是最终返回的初始值;queue
是更新队列,当我们多次更新某一状态时需要通过queue
队列存取和遍历;next
用来连接下一个hook
; - 将当前的
hook
连接到当前的fiberNode
的hook
链表上 - 绑定状态更新方法
dispatchAction
,并返回[state, dispatchAction]
function mountState(initailState){
var hook = {
moemoizedState: null,
queue: {
pending: null,
dispatch: null,
},
next: null
}
...
var dispatch = queue.dispatch = dispatchAction.bind(null, currentFiber,queue);
return [hook.memoizedState,dispatch];
}
useEffect -- mountEffectImpl
-
在
mountEffect
中,首先将当前的hook挂载到当前fiberNode
的hook
链表上 -
由于
useEffect
是异步执行的,会产生专属于useEffect
的hook
。此时会将产生的useEffect
产生的hook存入componentUpdateQueue
更新队列中。在某一次页面渲染结束后会去遍历这个更新队列,执行传入的effect hook
。
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 创建并获取当前hook节点信息
var hook = mountWorkInProgressHook();
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
}
function mountWorkInProgressHook() {
// 将当前hook连接到我们的hook链表中
var hook = {
memoizedState: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag, // 更新标识
create: create, // 传入的回调,也就是我们开发时的第一个参数
destroy: destroy, // return 的函数,组件销毁时执行的函数
deps: deps, // 依赖项数组
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 这里做的就是把每个useEffect hook单独以链式结构存到了componentUpdateQueue这个全局变量中
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}
初始化渲染至此结束,此时fiberNode
的hook
链式结构是
currentFiber: {
...
memoizedState: {
memoizedState: xxx,
...
next: {
memoizedState: xxx,
...
next: {
memoizedState: xxx,
...
next:hook4
}
}
}
}
hook组件更新阶段
在组件更新时,于初始化类似,将更新的对应hook进行挂载,根据链表依次执行hook。在此时,需要执行状态更新和useEffect
更新,最后更新完成。
useState
在初始化阶段有提到,mountState
阶段会绑定dispatchAction
并作为参数返回,其实也就是使用useState
时返回的setXXX
。
而在dispatchAction
中实际上,做了两件事
- 创建update节点,并连接到
useState
的queue
后面,这样每次调用dispatchAction
都会在后面连接一个update
节点,从而生成一个更新队列。 - 然后根据更新任务的优先级排列任务,最后遍历整个fiber树执行更新操作。
执行setXXX后,发生了什么?
在初始化阶段讲到的,根据是否存在dispatcher,赋值。
{
// 更新阶段dispatcher已存在,执行else部分
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}
而在HooksDispatcherOnMountInDEV
中各种hooks
对应的是update
方法,useState
对应的是updateState
方法。
function updateState(initialState){ return updateReducer(basicStateReducer);}
可以看到,updateState
实际上是返回了一个reducer
-
在
updateReducer
中,获取到hook的更新队列,比如执行了三次setCount
,则队列中就会存在三项。 -
拿到更新队列后,会对其进行循环遍历,计算赋值,最终会将最新的state值复制到hook的
memoizedState
上并返回。
function updateReducer(reducer, initialArg, init) { // 获取到当前hook,其实也就是直接.next就可以 var hook = updateWorkInProgressHook(); var queue = hook.queue; // 取到待更新的队列 var pendingQueue = queue.pending; // 如果待更新队列不为空,那么遍历处理 if (pendingQueue !== null) { var first = pendingQueue.next; var newState = null; var update = first; queue.pending = null; // 循环遍历,是更新阶段的核心和关键, do { var action = update.action; newState = reducer(newState, action); update = update.next; } while (update !== null && update !== first); // 最新的状态值赋值给memoizedState hook.memoizedState = newState; } // 将状态值和更新方法返回,就和初次渲染一样的流程 var dispatch = queue.dispatch; return [hook.memoizedState, dispatch];}
如何形成更新队列?
而在updateReducer
之前,会执行dispatchAction
,将每个update
加入到更新队列中。
// dispatchAction核心代码var pending = queue.pending;// 这里是链表创建和连接的核心if (pending === null) { update.next = update;} else { update.next = pending.next; pending.next = update;}queue.pending = update;
每次dispatch
时,当目前更新队列为空时,将当前update
加入,next
指向自己
当不为空时,将当前update
放到更新队列最后一项的next
上。
需要注意的是,更新队列中的first指向第一个执行的update
,queue.pending
指向最后一个。
所以在更新队列循环执行时,结束循环的条件为while (update !== null && update !== first);
也就是当前update
为更新队列中的第一个也就是唯一一个update
。
useEffect
与useState
相似,在更新过程中会执行updateEffect
,在其中执行updateEffectImpl
在updateEffectImpl
中,无论依赖项是否更改,都会调用pushEffect方法。
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { // 获取到当前hook var hook = updateWorkInProgressHook(); // 比较依赖项是否发生了变化 if (areHookInputsEqual(nextDeps, prevDeps)) { // 如果相同则不对当前hook的属性进行更新 pushEffect(hookEffectTag, create, destroy, nextDeps); return; } // 如果依赖项发生了变化,更新当前hook的memoizedState,这里的赋值只是做一个记录,并没有实际意义 currentlyRenderingFiber$1.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, destroy, nextDeps);}
而在pushEffect
方法中,
- 创建一个
effect
对象,并返回 - 创建/更新
componentUpdateQueue
队列,其中存储了useEffect
产生的回调,componentUpdateQueue
队列不存在的话会进行创建,如果存在,会和mountState
阶段一样创建一个effect
的循环链表。effect
对象中的tag就是用来判断这个useEffect
回调是否需要被执行。
一些可以被解释了的问题
hook必须按照固定顺序调用,不能在条件判断中调用
由于每一个hook都是通过next指针在链表中按顺序连接的,如果在某个条件判断的情况下,某个hook不存在,就会导致整个hook链表中断,没法继续正常遍历hook链表,产生问题。