2021SC@SDUSC
上一篇文章我们介绍了react-hooks初始化都做些什么,接下来,我们分析一下,对于更新阶段,说明上一次 workInProgress 树已经赋值给了 current 树。存放hooks信息的memoizedState,此时已经存在current树上,react对于hooks的处理逻辑和fiber树逻辑类似。
对于一次函数组件更新,当再次执行hooks函数的时候,比如 useState(0) ,首先要从current的hooks中找到与当前workInProgressHook,对应的currentHooks,然后复制一份currentHooks给workInProgressHook,接下来hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失。
所以函数组件每次更新,每一次react-hooks函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook,我们接下来一起看这个updateWorkInProgressHook。
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
let nextCurrentHook: null | Hook;
if (currentHook === null) {/* 如果 currentHook = null 证明它是第一个hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一个hooks,那么指向下一个 hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {//第一次执行hooks
// 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 */
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {//创建一个新的hook
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {// 重新更新 hook
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
mount时获取当前hook使用的是mountWorkInProgressHook,而update时使用的是updateWorkInProgressHook,这里的原因是:
1.mount时可以确定是调用ReactDOM.render或相关初始化API产生的更新,只会执行一次。
2.update可能是在事件回调或副作用中触发的更新或者是render阶段触发的更新,为了避免组件无限循环更新,后者需要区别对待。
如果说mount时这两者还有区别,那update时,useReducer与useState调用的则是同一个函数updateReducer。
function updateState(
initialState: (() => S) | S,
): [S, Dispatch>] {
return updateReducer(basicStateReducer, (initialState: any));
}
调用的updateReducer函数如下:
function updateReducer(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
invariant(
queue !== null,
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
if (__DEV__) {
if (current.baseQueue !== baseQueue) {
// Internal invariant that should never happen, but feasibly could in
// the future if we implement resuming, or some form of that.
console.error(
'Internal error: Expected work-in-progress queue to be a clone. ' +
'This is a bug in React.',
);
}
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
const clone: Update = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
整个流程可以概括为一句话:找到对应的hook,根据update计算该hook的新state并返回。
此外,我们通常认为,useReducer(reducer, initialState)的传参为初始化参数,在以后的调用中都不可变。
但是在updateReducer方法中,可以看到lastRenderedReducer在每次调用时都会重新赋值。
queue.lastRenderedReducer = reducer;
也就是说,reducer参数是随时可变的。
function updateEffect(create, deps): void {
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(hookEffectTag, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
destroy,
nextDeps,
);
}
useEffect 做的事很简单,判断两次deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect,这里注意 effect的标签,hookEffectTag,如果不相等,那么更新 effect ,并且赋值给hook.memoizedState,这里标签是 HookHasEffect | hookEffectTag,然后在commit阶段,react会通过标签来判断,是否执行当前的 effect函数。
function updateMemo(
nextCreate,
deps,
) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 之前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
在组件更新过程中,我们执行useMemo函数,做的事情实际很简单,就是判断两次 deps是否相等,如果不相等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState,如果相等 证明没有依赖项改变,那么直接获取缓存的值。
不过这里有一点,值得注意,nextCreate()执行,如果里面引用了usestate等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。
function updateCallback(callback: T, deps: Array | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看出updateCallback和updateMemo十分相像,仅仅是缺少一句 const nextValue = nextCreate();这里就不再多说了
function updateRef(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
函数组件更新useRef做的事情就是返回了缓存下来的值,换句话说,无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象,当然这也解释了在useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。