前言
在React 18
中,我们详细分析了各种类型,比如HostComponent
、FunctionComponent
的初次渲染和渲染更新的流程,但是我们还没仔细分析过React
重要的React Hooks
相关源码
- 比如最常见的
useEffect()
- 比如提供全局数据的
useContext()
- 比如性能优化经常使用的
useMemo()
、useCallback()
在本文中,我们将针对这些常见的hooks
,侧重于useEffect()
和useLayoutEffect()
,详细分析React Hooks
初次渲染和渲染更新的流程
整体流程图(TODO)
1. useEffect()
从下面useEffect()
的使用中,我们可以知道,主要分为两个部分
()=> {}
: 回调函数[color]
: 依赖项数组
useEffect(() => {
console.log("useEffect 执行: 当前颜色", color);
// 模拟一个可能引起布局变化的操作(但会有延迟)
const element = document.getElementById("box");
if (element) {
element.style.transform = "translateX(100px)";
}
return () => {
console.log("Effect destroyed");
};
}, [color]);
而从React 18
的源码中,我们可以发现,跟useState()
类似,useEffect()
也会根据初始化或者更新时选择不同的方法
var HooksDispatcherOnMount = {
//...
useEffect: mountEffect,
useLayoutEffect: mountLayoutEffect,
//...
};
var HooksDispatcherOnUpdate = {
//...
useEffect: updateEffect,
useLayoutEffect: updateLayoutEffect,
//...
};
初始化时,useEffect
本质就是mountEffect()
更新时,useEffect
本质就是updateEffect()
function mountEffect(create, deps) {
return mountEffectImpl(Passive | PassiveStatic, Passive$1, create, deps);
}
function updateEffect(create, deps) {
return updateEffectImpl(Passive, Passive$1, create, deps);
}
从mountEffectImpl()
和updateEffectImpl()
的代码可以知道,其实本质都是
- 使用
mountWorkInProgressHook()
或者updateWorkInProgressHook()
获取当前的hook
- 更新阶段使用
areHookInputsEqual(nextDeps, prevDeps)
判断依赖是否已经改变,如果改变,则打上HasEffect|Passive
的flags
,为后面的commit
阶段做准备;如果依赖项没有改变,则打上flags
为Passive
,为后面的commit
阶段做准备 - 最终还是需要执行一次
pushEffect()
重新构建一次fiber.updateQueue
单循环链表结构
区别于依赖项改变,则传入
pushEffect()
的hookFlags
不同
在下面的小节,我们将针对一些方法进行详细的分析
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber$1.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var destroy = undefined;
if (currentHook !== null) {
var prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
var prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber$1.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HasEffect | hookFlags,
create,
destroy,
nextDeps
);
}
1.1 mountWorkInProgressHook
- 创建一个新的
hook
- 如果当前fiber的hook集合,也就是
fiber.memoizedState
这个存储单链表数据为空,则进行头节点的赋值,否则进行workInProgressHook.next = hook
构建单链表结构 - 最终返回当前fiber的hook集合的最后一个元素
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
1.2 updateWorkInProgressHook
在hooks&更新渲染流程分析
的文章分析中,我们已经分析过updateWorkInProgressHook()
获取当前fiber的hook
源码的代码过于繁琐和难以看懂,可以简化为下面代码
因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)
这里考虑一般情况下=>可以简化代码逻辑,容易理解
由于是更新阶段,因此currentlyRenderingFiber$1.alternate
必定存在,复用当前alternate
的memoizedState
构建链表数据,主要是头节点的赋值
+剩余节点的赋值
两个步骤
涉及到三个全局变量的赋值:
currentHook
: 代表的是当前renderFiber.alternate
对应的hook
currentlyRenderingFiber.memoizedState
: 当前renderFiber.alternate
复制的单链表workInProgressHook
: 当前renderFiber
对应的hook
fiber.memoizedState
跟hook.memoizedState
是不一样的!!
function updateWorkInProgressHook() {
const current = currentlyRenderingFiber.alternate;
if (workInProgressHook === null) {
// 头节点还没赋值,获取当前fiber的alternate对应的单循环链表结构,因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)
currentlyRenderingFiber.memoizedState = current.memoizedState;
workInProgressHook = currentlyRenderingFiber.memoizedState;
currentHook = current.memoizedState;
} else {
// 当前fiber的下一个hook,因为一个fiber可能存在多个hook,会形成一个单循环链表结构
workInProgressHook = workInProgressHook.next;
currentHook = currentHook.next;
}
return workInProgressHook;
}
1.3 pushEffect重新构建fiber.updateQueue
传入的参数值中
tag
:HasEffect | Passive
,HasEffect
代表依赖项已经改变或者首次渲染,当检测到HasEffect
,需要重新执行create()
方法;Passive
代表该fiber存在hook
,在销毁时调用effect.destory()
可以通过这个flags
进行判断create
:useEffect(create, [deps])
的执行方法destroy
: 在commitHookEffectListMount()
中进行destroy
=create()
赋值(首次渲染阶段不会触发destroy()
),它会在调用一个新的effect
执行之前对前一个effect
进行清理deps
:useEffect()
的依赖项,依赖项变化时,会重新执行传入的create()
方法
destroy()
的执行将在下面的小节展开分析
fiber.updateQueue
存放着hook
的单链表数据,如果为空,则通过createFunctionComponentUpdateQueue()
创建出新的fiber.updateQueue
注意:
fiber.updateQueue
是一个对象,具备lastEffect
和stores
两个属性
如果fiber.updateQueue
已经初始化完成,则按照单链表的形式不断next
即可
lastEffect
代表单链表的最后一个hook
firstEffect
代表单链表的最前面的一个hook
lastEffect.next
=firstEffect
构成循环结构
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null,
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
function createFunctionComponentUpdateQueue() {
return {
lastEffect: null,
stores: null,
};
}
1.3 commit提交阶段处理effect
commit阶段
最终是提交fiber Root
的复制fiber
进行提交,然后触发finishConcurrentRender()
方法,从而最终触发commitRootImpl()
方法
var finishedWork = root.current.alternate;
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
//...
case RootCompleted: {
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
}
}
function commitRoot(root, recoverableErrors, transitions) {
//...
commitRootImpl(root, recoverableErrors, transitions, previousUpdateLanePriority);
}
而commitRootImpl()
的核心代码如下所示:
- 判断
subtreeHasEffects
(root元素的children存在effect
/存在Placement
等flags)和rootHasEffect
(root元素存在effect
/存在Placement
等flags)然后调用commitBeforeMutationEffects()
commitMutationEffects()
commitLayoutEffects()
- 调用异步更新
ensureRootIsScheduled()
- 调用同步更新
flushSyncCallbacks()
- 微任务触发
flushPassiveEffects()
触发effect
相关处理
function commitRootImpl() {
//...
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback$2(NormalPriority, function () {
flushPassiveEffects();
return null;
});
}
}
var subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
var rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
commitBeforeMutationEffects(root, finishedWork);
commitMutationEffects(root, finishedWork, lanes);
commitLayoutEffects(finishedWork, root, lanes);
executionContext = prevExecutionContext;
} else {
//...no effects
}
ensureRootIsScheduled(root, now());
flushSyncCallbacks();
}
从上面代码可以看出
- 同步执行了
commitLayoutEffects()
,关联LayoutEffect
相关逻辑 - 异步执行了
flushPassiveEffects()
,关联Effect
相关逻辑
1.3.1 flushPassiveEffects()
由于flushPassiveEffects()
的代码过于繁杂,使用一个流程图进行展示
最终触发的是
commitHookEffectListUnmount()
: 触发effect.destory()
commitHookEffectListMount()
: 触发effect.create()
在删除fiber的逻辑中,使用PassiveMask=Passive|ChildDeletion
作为判断触发commitHookEffectListUnmount()
在清除上一个effect的逻辑中,使用Passive
作为判断来触发commitPassiveUnmountOnFiber()
->commitHookEffectListUnmount()
在触发effect.create()
时,先使用PassiveMask
进行筛选,然后使用Passive
作为判断来触发commitPassiveMountOnFiber()
function commitPassiveMountEffects_begin(
subtreeRoot,
root,
committedLanes,
committedTransitions
) {
while (nextEffect !== null) {
var fiber = nextEffect;
var firstChild = fiber.child;
if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
commitPassiveMountEffects_complete(
subtreeRoot,
root,
committedLanes,
committedTransitions
);
}
}
}
function commitPassiveMountEffects_complete(
subtreeRoot,
root,
committedLanes,
committedTransitions
) {
while (nextEffect !== null) {
var fiber = nextEffect;
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveMountOnFiber(
root,
fiber,
committedLanes,
committedTransitions
);
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
var sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
2. useLayoutEffect()
从下面代码可以看出,useLayoutEffect()
与useEffect()
的初始化流程几乎一模一样,区别在于传入的参数中
fiberFlags
为Update
hookFlags
为Layout
而useEffect()
的传入参数为
fiberFlags
为Passive
hookFlags
为Passive
注: 两个
Passive
为不同的数据...虽然名称一模一样
function mountLayoutEffect(create, deps) {
var fiberFlags = Update;
//...
return mountEffectImpl(fiberFlags, Layout, create, deps);
}
function updateLayoutEffect(create, deps) {
return updateEffectImpl(Update, Layout, create, deps);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber$1.flags |= fiberFlags;
hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var destroy = undefined;
if (currentHook !== null) {
var prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
var prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber$1.flags |= fiberFlags;
hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
}
2.1 commit提交阶段处理commitLayoutEffects
从上面useEffect()
的commit阶段
分析可以知道,commit阶段
最终是提交fiber Root
的复制fiber
进行提交,然后触发finishConcurrentRender()
方法,从而最终触发commitRootImpl()
方法
而commitRootImpl()
的核心代码如下所示:
- 判断
subtreeHasEffects
(root元素的children存在effect
/存在Placement
等flags)和rootHasEffect
(root元素存在effect
/存在Placement
等flags)然后调用commitBeforeMutationEffects()
commitMutationEffects()
commitLayoutEffects()
- 调用异步更新
ensureRootIsScheduled()
- 调用同步更新
flushSyncCallbacks()
- 微任务触发
flushPassiveEffects()
触发effect
相关处理
function commitLayoutEffects(finishedWork, root, committedLanes) {
commitLayoutEffects_begin(finishedWork, root, committedLanes);
}
function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
while (nextEffect !== null) {
var fiber = nextEffect;
var firstChild = fiber.child;
//...
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
// LayoutMask = Update | Callback | Ref | Visibility
firstChild.return = fiber;
nextEffect = firstChild;
} else {
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
while (nextEffect !== null) {
var fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
var current = fiber.alternate;
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
var sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
主要逻辑还是封装在commitLayoutEffectOnFiber()
中,也是根据不同的fiber.tag
进行不同方法的调用
FunctionComponent
: 触发commitHookEffectListMount()
ClassComponent
: 先触发componentDidMount
/componentDidUpdate
,然后触发commitUpdateQueue()
HostRoot
: 触发commitUpdateQueue()
HostComponent
: 触发commitMount()
function commitLayoutEffectOnFiber() {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
break;
}
case ClassComponent: {
var instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
instance.componentDidMount();
} else {
var prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(
finishedWork.type,
current.memoizedProps
);
var prevState = current.memoizedState; // We could update instance props and state here,
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
);
}
}
}
var updateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostRoot: {
var _updateQueue = finishedWork.updateQueue;
if (_updateQueue !== null) {
var _instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
_instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
_instance = finishedWork.child.stateNode;
break;
}
}
commitUpdateQueue(finishedWork, _updateQueue, _instance);
}
break;
}
case HostComponent: {
var _instance2 = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
var type = finishedWork.type;
var props = finishedWork.memoizedProps;
commitMount(_instance2, type, props);
}
break;
}
case HostText: {
// We have no life-cycles associated with text.
break;
}
}
}
if (!offscreenSubtreeWasHidden) {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
2.1.1 commitHookEffectListMount()
function commitHookEffectListMount(flags, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
var create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
从fiber.updateQueue
取出当前fiber的hook单循环链表数据,不断遍历取出对应create
方法执行,然后返回对应的destroy方法给hook.destory
create()就是下面
useEffect(fn, [color])
的fn,destroy就是返回的console.log("Effect destroyed")
对应的方法
useEffect(() => {
return () => {
console.log("Effect destroyed");
};
}, [color]);
2.1.2 commitMount()
进行dom.focus()
或者dom.src
的赋值
function commitMount(domElement, type, newProps, internalInstanceHandle) {
switch (type) {
case "button":
case "input":
case "select":
case "textarea":
if (newProps.autoFocus) {
domElement.focus();
}
return;
case "img": {
if (newProps.src) {
domElement.src = newProps.src;
}
return;
}
}
}
2.1.3 commitUpdateQueue()
我们从上面的分析可以知道,ClassComponent
会先触发componentDidMount
/componentDidUpdate
,然后触发commitUpdateQueue()
我们可以构建一个如下的示例,当点击按钮触发this.setState()
时会触发commitUpdateQueue()
,如下面截图所示
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
// 触发 setState 并传入回调函数
this.setState({ count: this.state.count + 1 }, () => {
console.error("Effect.callback executed!", this.state.count);
});
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
而
setState
的回调什么时候放入到effect.callback
中呢?在下面的小节我们将简单分析下
ClassComponent的setState
流程
2.2 ClassComponent的setState()
从下面代码我们可以看出,setState()
传入的回调放在了update.callback
中
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, "setState");
};
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
var root = enqueueUpdate$1(fiber, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitions(root, fiber, lane);
}
},
};
那么
update.callback
是什么时候转移到fiber.updateQueue.callback
中呢?
如下图所示,在beginWork()
->case ClassComponent: 触发updateClassComponent()
->processUpdateQueue()
在processUpdateQueue()
中,会将update
放入到fiber.updateQueue.effects
中
而在上面的Commit阶段中,我们会触发commitUpdateQueue()
,如下面代码所示,也就是从fiber.updateQueue
取出effects
然后进行处理
function commitUpdateQueue(finishedWork, finishedQueue, instance) {
// Commit the effects
var effects = finishedQueue.effects;
finishedQueue.effects = null;
if (effects !== null) {
for (var i = 0; i < effects.length; i++) {
var effect = effects[i];
var callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
}
}
}
至此,我们弄清楚了ClassComponent
中如何触发LayoutEffect
以及setState
会触发什么流程的大体逻辑
3. useMemo()
跟useEffect()
/useLayoutEffect()
的逻辑基本一致
- 获取当前
fiber
的hook
- 判断当前依赖是否发生了改变,如果已经发生了改变,则重新触发
nextCreate()
获取新的值存入hook.memoizedState
function mountMemo(nextCreate, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo(nextCreate, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
var nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
4. useCallback()
跟useEffect()
/useLayoutEffect()
的逻辑基本一致
- 获取当前
fiber
的hook
- 判断当前依赖是否发生了改变,如果已经发生了改变,则更新当前传入的
callback
方法,否则返回上一次得到的callback
方法
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback(callback, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
5. useRef()
比useEffect()
/useLayoutEffect()
的逻辑更为简单
- 获取当前
fiber
的hook
- 缓存传入的值,更新时直接返回缓存的值,而不是重新创建
改变 ref 不会触发重新渲染。这意味着 ref 是存储一些不影响组件视图输出信息的完美选择。例如,如果需要存储一个 interval ID 并在以后检索它,那么可以将它存储在 ref 中。只需要手动改变它的 current 属性 即可修改 ref 的值
使用 ref 可以确保:
- 可以在重新渲染之间 存储信息(普通对象存储的值每次渲染都会重置)。
- 改变它 不会触发重新渲染(状态变量会触发重新渲染)。
- 对于组件的每个副本而言,这些信息都是本地的(外部变量则是共享的)
function mountRef(initialValue) {
var hook = mountWorkInProgressHook();
var _ref2 = {
current: initialValue,
};
hook.memoizedState = _ref2;
return _ref2;
}
function updateRef(initialValue) {
var hook = updateWorkInProgressHook();
return hook.memoizedState;
}
6. useContext()
使用Context的三种场景: FunctionComponent、ClassComponent、Context.Consumer
Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递
需要多个步骤:
createContext()
创建一个Context
对象数据ThemeContext
- 在最外层包裹
<Context.Provider value=>
传递对应的数据 - 在某一个内层使用
const { theme, toggleTheme } = useContext(ThemeContext)
获取对应的Context
数据,然后进行使用 - 部分使用场景,比如
<Context.Consumer>
可以直接获取对应的(Context数据)=>{}
进行操作
这里可能有点疑问,都直接使用
useContext(ThemeContext)
获取对应的Context
数据,为什么不直接就const {} = ThemeContext
直接使用该数据呢?那是因为
React
的useContext
还进行了ThemeContext
和当前fiber
的额外处理,当ThemeContext
变化时会触发当前fiber
进行重新渲染...可能跟正常使用JS/使用Vue监听渲染相比较,这种做法会比较奇怪
const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {}, // 占位函数
});
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<FunctionComponentDemo />
<ClassComponentDemo />
<ConsumerComponentDemo />
</ThemeContext.Provider>
const { theme, toggleTheme } = useContext(ThemeContext);
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<div style={{ background: theme === 'dark' ? '#333' : '#fff', padding: '20px' }}>
<h2>Context.Consumer Component</h2>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)}
</ThemeContext.Consumer>
接下来我们将针对上面4个步骤进行详细的分析
6.1 React.createContext()
创建一个context
的对象数据,传入defaultValue
赋值给_currentValue
function createContext(defaultValue) {
var context = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null,
_defaultValue: null,
_globalName: null,
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
return context;
}
6.2 <Context.Provider>
当使用 <Context.Provider> 时,JSX 会被 Babel 或 TypeScript 转换为 React.createElement 调用:
// JSX: <Context.Provider value={value}>...</Context.Provider>
// 转换为:
React.createElement(Context.Provider, { value }, children);
生成的 React 元素(对象)结构为:
{
$$typeof: Symbol(react.element),
type: Context.Provider, // 关键属性
props: { value, children },
// ...其他元数据
}
由createContext()
可以知道,context.Provider
为:
Context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
因此生成的 React 元素(对象)结构实际为:
{
$$typeof: Symbol(react.element),
type: {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
},
props: { value, children },
}
后续流程会创建fiber
=>createFiberFromTypeAndProps()
由于type.$$typeof=REACT_PROVIDER_TYPE
,从而命中fiberTag=ContextProvider
,也就是tag=ContextProvider
function createFiberFromTypeAndProps() {
var fiberTag = IndeterminateComponent;
var resolvedType = type;
if (typeof type === "function") {
//...
} else if (typeof type === "string") {
//...
} else {
getTag: switch (type) {
//...
default: {
if (typeof type === "object" && type !== null) {
switch (type.$$typeof) {
case REACT_PROVIDER_TYPE:
fiberTag = ContextProvider;
break getTag;
//...
}
}
}
}
}
var fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}
beginWork()
命中ContextProvider
=> updateContextProvider()
tag=ContextProvider
beginWork() {
switch (workInProgress.tag) {
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
}
}
updateContextProvider()
的逻辑也比较简单:
pushProvider()
更新目前Context最新的值到栈中,并且更新context._currentValue
的值- 比较
组件即将应用的新的props
和组件在上一次渲染中已经应用的props
,如果值没有变化,则bailoutOnAlreadyFinishedWork()
什么都不操作
bailoutOnAlreadyFinishedWork()
一般返回null,这样代表当前fiber不需要更新,后续操作也不需要考虑该fiber
如果
props.value
已经改变,如下图所示,也就是创建createContext(value)
的value
发生改变,则触发propagateContextChange()
通知所有消费该Context的fiber进行更新渲染然后跟其它类型一样,正常触发
reconcileChildren()
进行diff,然后返回workInProgress.child
function updateContextProvider(current, workInProgress, renderLanes) {
var providerType = workInProgress.type;
var context = providerType._context;
var newProps = workInProgress.pendingProps;
var oldProps = workInProgress.memoizedProps;
var newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
if (oldProps !== null) {
var oldValue = oldProps.value;
if (objectIs(oldValue, newValue)) {
// No change. Bailout early if children are the same.
if (oldProps.children === newProps.children && !hasContextChanged()) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes
);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, renderLanes);
}
}
var newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
我们是如何拿到目前Context最新的值的呢?
providerType = workInProgress.type
又是代表什么意思?
type
就是createContext()
创建的context.Provider
,里面存放着各种数据,包括目前状态值,通过providerType._context
就可以拿到当前的Context
对象数据!
{
$$typeof: Symbol(react.element),
type: {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
},
props: { value, children },
}
接下来的小节中,我们将针对
pushProvider()
和propagateContextChange()
展开分析
6.2.1 pushProvider()
Context.Provider
会存放在一个valueStack
的栈中,并且这个栈有个特点
- 最新一个数据会直接放在
cursor.current
中 - 倒数第二新的元素才会在栈顶上
比如:正常来说,valueCursor=[1, 2, 3, 4, 5]
但是目前的情况是
cursor.current
=5
valueCursor=[1, 2, 3, 4]
当加入新的元素6
时,会变成
cursor.current
=6
valueCursor=[1, 2, 3, 4, 5]
而对于 <Context.Provider>
,多个不同的 <Context.Provider>
会使用同一个 valueCursor
,这个层级又加了一层,如下面代码所示:
- 当前最新的
<Context.Provider>
的值放在valueCursor.current
并且更新到context._currentValue
中 - 第二新的值移动到
valueStack
的栈中
初始化valueCursor = { current: null }
,每次调用 pushProvider()
更新目前Context最新的值到栈中,并且更新 valueCursor.current
和 context._currentValue
的值
var valueStack = [];
var index = -1;
var valueCursor = createCursor(null);
function createCursor(defaultValue) {
return {
current: defaultValue,
};
}
function pushProvider(providerFiber, context, nextValue) {
push(valueCursor, context._currentValue);
context._currentValue = nextValue;
}
function push(cursor, value, fiber) {
index++;
valueStack[index] = cursor.current;
cursor.current = value;
}
6.2.2 propagateContextChange()
由于涉及到useContext()
注册fiber的相关监听,因此这里暂时不分析,放在6.3.2 propagateContextChange()
展开分析
6.3 useContext()
useContext()
实际的方法名称是readContext()
,逻辑也比较简单:
- 构建一个
firstContext
对象,具备Context
这个对象、Context._currentValue
这个对象的值、next
(单链表结构), 然后赋值给当前的fiber.dependencies
- 如果本身
currentlyRenderingFiber.dependencies
已经有值,则持续构建单链表数据即可(当前fiber可能有多个不同的Context)
function readContext(context) {
var value = context._currentValue;
if (lastFullyObservedContext === context);
else {
var contextItem = {
context: context,
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
} else {
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
上面代码中
lastFullyObservedContext
和currentlyRenderingFiber
又是在哪里赋值的呢?
这得说到一个方法,在多个渲染时触发,比如FunctionComponent
中renderWithHooks()
之前会触发prepareToReadContext()
在这个方法中,会进行lastFullyObservedContext
和currentlyRenderingFiber
的赋值以及fiber.dependencies
的清理
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
prepareToReadContext(workInProgress, renderLanes);
//...
value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
//...
}
function prepareToReadContext(workInProgress, renderLanes) {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
lastFullyObservedContext = null;
var dependencies = workInProgress.dependencies;
if (dependencies !== null) {
var firstContext = dependencies.firstContext;
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
// Context list has a pending update. Mark that this fiber performed work.
markWorkInProgressReceivedUpdate();
} // Reset the work-in-progress list
dependencies.firstContext = null;
}
}
}
6.3.1 propagateContextChange()标记子fiber需要更新
分析完成useContext()
,我们可以回到<Context.Provider>
传入的值改变时触发的通知方法propagateContextChange()
下面的代码比较长,但是实际内容是比较简单的,本质就是一个深度遍历
从当前fiber.child
开始,遍历它的fiber.dependencies
(单链表,多个Context),看看是否注册了当前的Context数据
遍历完fiber.child
,就遍历它的fiber.child.sibling
,深度遍历所有的子fiber,检查对应的dependencies
(单链表,多个Context)是否注册了当前的Context数据
当fiber.dependencies
中有注册了当前的Context数据
时
- 通过
mergeLanes(fiber.lanes, renderLanes)
将当前renderLanes
合并到当前fiber的lanes中 - 通过
scheduleContextWorkOnParentPath()
将当前renderLanes
不断向上传导更新到当前fiber的parentFiber的childLanes
中
父 Fiber 的 lanes 不为空但子 Fiber 的 lanes 为空时,父 Fiber 的更新可能会导致父组件重新渲染,而子组件可能被跳过不渲染
比如子 Fiber 的
workInProgress.lanes & renderLanes === NoLanes
=====> 子 Fiber 的优先级(lanes)与当前渲染批次(renderLanes)无交集
如果当前fiber是ClassComponent
类型,则创建一个update
update.lane
:renderLanes
的最高优先级laneupdate.tag
:ForceUpdate
跟ClassComponent的setState()操作一样,
setState()
也会创建一个新的update
,压入到队列中
??????但是setState()是如何触发更新的??
function propagateContextChange(workInProgress, context, renderLanes) {
propagateContextChange_eager(workInProgress, context, renderLanes);
}
function propagateContextChange_eager(workInProgress, context, renderLanes) {
var fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
while (fiber !== null) {
var nextFiber = void 0;
var list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
var dependency = list.firstContext;
while (dependency !== null) {
if (dependency.context === context) {
if (fiber.tag === ClassComponent) {
var lane = pickArbitraryLane(renderLanes);
var update = createUpdate(NoTimestamp, lane);
update.tag = ForceUpdate;
var updateQueue = fiber.updateQueue;
if (updateQueue === null);
else {
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
var alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress
);
list.lanes = mergeLanes(list.lanes, renderLanes);
break;
}
dependency = dependency.next;
}
} else if (fiber.tag === ContextProvider) {
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (fiber.tag === DehydratedFragment) {
//...
} else {
nextFiber = fiber.child;
}
if (nextFiber !== null) {
nextFiber.return = fiber;
} else {
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
nextFiber = null;
break;
}
var sibling = nextFiber.sibling;
if (sibling !== null) {
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
nextFiber = nextFiber.return;
}
}
fiber = nextFiber;
}
}
6.3.2 子fiber触发更新
当我们使用下面的调试代码时,经过调试,我们可以知道,如果 FunctionComponentDemo
不使用 useContext()
,那么即使父fiber改变了 theme
,那么 FunctionComponentDemo
也不会触发更新
当使用 useContext()
后,父fiber改变了 theme
, FunctionComponentDemo
会触发更新
那么究竟是如何触发更新的呢?
const ThemeContext = React.createContext('light');
const FunctionComponentDemo = React.memo(() => {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
});
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<div>
<ThemeContext.Provider value={theme}>
<FunctionComponentDemo/>
</ThemeContext.Provider>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)
}
const root = createRoot(document.getElementById('root'));
root.render(<App/>);
由上面 6.3.1 propagateContextChange()标记子fiber需要更新
,我们可以知道,当使用 useContext()
后,父fiber改变了 theme
,那么 FunctionComponentDemo
所在的 fiber.lanes
就会被标记上更新lane !
在 父fiber改变了 theme
-> 触发 父fiber执行 beginWork()
后,会触发 子fiber执行 beginWork()
,从而执行 updateFunctionComponent()
在 updateFunctionComponent()
中,渲染更新流程如果 didReceiveUpdate
= false
,说明没有变化,则不继续执行 diff 和生成 childrenFibers 的流程,也就是不需要该 fiber 进行渲染更新!
function updateFunctionComponent() {
prepareToReadContext(workInProgress, renderLanes);
nextChildren = renderWithHooks();
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue;
workInProgress.flags &= ~(Passive | Update);
current.lanes = removeLanes(current.lanes, lanes);
}
但是在 prepareToReadContext()
,我们会检测 fiber.dependencies
是否存在,并且 includesSomeLane(dependencies.lanes, renderLanes)
而这两个条件恰恰是 propagateContextChange()
所做的事情,因此会触发 markWorkInProgressReceivedUpdate()
设置 didReceiveUpdate
= true
,从而继续向下执行 diff 和生成 childrenFibers 的流程,触发更新!
function prepareToReadContext(workInProgress, renderLanes) {
currentlyRenderingFiber = workInProgress;
var dependencies = workInProgress.dependencies;
if (dependencies !== null) {
var firstContext = dependencies.firstContext;
if (firstContext !== null) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
markWorkInProgressReceivedUpdate();
}
dependencies.firstContext = null;
}
}
}
6.4 </Context.Provider>
completeWork进行popProvider()
6.5 <Context.Consumer>
由createContext()
可以知道,updateContextConsumer()
拿到的type
就是context
这里省略了根据
$$typeof: REACT_CONTEXT_TYPE
设置对应的tag=ContextConsumer
的代码展示
function createContext(defaultValue) {
var context = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null,
_defaultValue: null,
_globalName: null,
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
return context;
}
beginWork() {
switch (workInProgress.tag) {
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
}
}
function updateContextConsumer(current, workInProgress, renderLanes) {
var context = workInProgress.type;
var newProps = workInProgress.pendingProps;
var render = newProps.children;
prepareToReadContext(workInProgress, renderLanes);
var newValue = readContext(context);
var newChildren = render(newValue);
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
在updateContextConsumer()
中,我们触发
prepareToReadContext()
: 清理fiber.dependencies
以及其它属性readContext()
: 进行Context
与当前fiber的绑定!- 构建一个
firstContext
对象,具备Context
这个对象、Context._currentValue
这个对象的值、next
(单链表结构), 然后赋值给当前的fiber.dependencies
- 如果本身
currentlyRenderingFiber.dependencies
已经有值,则持续构建单链表数据即可(当前fiber可能有多个不同的Context)
- 构建一个
newChildren = render(newValue)
: 本质就是<ThemeContext.Consumer>
包裹的渲染函数,如下面的代码所示,包裹的渲染函数传入context对象
={ theme, toggleTheme }
- 跟其它fiber一样,触发
reconcileChildren()
进行diff,然后返回workInProgress.child
function ConsumerComponentDemo() {
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<div style={{ background: theme === 'dark' ? '#333' : '#fff', padding: '20px' }}>
<h2>Context.Consumer Component</h2>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
)}
</ThemeContext.Consumer>
);
}
render = function (_ref) {
var theme = _ref.theme,
toggleTheme = _ref.toggleTheme;
return React.createElement(
"div",
{
style: {
background: theme === "dark" ? "#333" : "#fff",
padding: "20px",
},
},
React.createElement("h2", null, "Context.Consumer Component"),
React.createElement("p", null, "Current Theme: ", theme),
React.createElement("button", { onClick: toggleTheme }, "Toggle Theme")
);
};
6.6 多个Context包裹的情况
场景 | 行为 |
---|---|
同类型 Context 嵌套 | 子组件获取最近的 Provider 的值,内层覆盖外层 |
不同类型 Context 嵌套 | 各 Context 独立,子组件可同时消费多个 Context 的值 |
Provider 未设置 value | 使用 createContext 的默认值,并抛出警告 |
外层 Provider 的 value 变化 | 不影响已嵌套在内层 Provider 的子组件(除非子组件依赖外层 Context) |
面对多个Context包裹的情况,比如下面代码所示,代码中是如何做到区分当前最近的Context的呢?
const ThemeContext = React.createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Parent>
<ThemeContext.Provider value="blue">
<Child />
</ThemeContext.Provider>
</Parent>
</ThemeContext.Provider>
);
}
对于不同的Context呢?比如下面代码所示,代码中是如何做到区分当前最近的Context的呢?
const ThemeContext = React.createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Parent>
<MyContext.Provider value="blue">
<Child />
</MyContext.Provider>
</Parent>
</ThemeContext.Provider>
);
}
区分多个嵌套Context使用的是valueStack
,valueStack 的作用是 管理嵌套的 Context Provider 的值,确保在组件树渲染过程中,每个层级的 Provider 能正确覆盖或恢复其关联的 Context 值
- 具体来说,当进入一个 Provider 组件时,React 会将当前 Context 的值压入栈(valueStack),然后更新为新的值
- 当退出该 Provider 的作用域时,再从栈中弹出旧值,恢复之前的 Context 值
不同的 Context 会共享同一个全局的 valueStack,但通过 栈的先进后出(FILO)特性 和 与当前处理的 Context 对象绑定,确保每个 Context 的值在嵌套层级中正确隔离和恢复
核心机制为:
- 全局的 valueStack:所有类型的 Context 在更新值时,共享同一个全局的 valueStack 数组。
- 操作顺序:每个 Context 的旧值在进入其 Provider 时被压入栈,退出时弹出恢复,严格遵循 嵌套顺序 => 通过
beginWork
和completeWork()
的执行顺序 - 上下文绑定:valueStack 仅存储值,而 值的归属(属于哪个 Context) 由代码执行顺序隐式保证
注:传入的newValue是目前
<FirstContext.Provider="newValue">
传入的newValue
,可能有多个<FirstContext.Provider="xxx">
会不断更新context._currentValue
并且我们要关注初始化时
createContext(defaultValue)
会进行context._currentValue = defaultValue
的赋值!多个不同的 Context 共用同一个 valueStack,比如
<FirstContext.Provider="newValue">
,然后<SecondContext.Provider="newValue">
都会压入值到 valueStack + valueCursor
const valueStack = [];
function pushProvider(context, newValue) {
var value = context._currentValue;
// 压入栈
index++;
valueStack[index] = cursor.current;
cursor.current = value;
// 更新context的值
context._currentValue = newValue; // 下一次popProvider()作为valueCursor的值
}
function popProvider(context) {
// 更新context的值
var currentValue = valueCursor.current;
if (currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) {
context._currentValue = context._defaultValue;
} else {
context._currentValue = currentValue;
}
// 弹出栈
if (index < 0) {
return;
}
cursor.current = valueStack[index]; // 下一次popProvider()作为valueCursor的值
valueStack[index] = null;
index--;
}
6.5.1 具体示例
const ThemeContext = React.createContext("light");
const UserContext = React.createContext("guest");
function App() {
return (
<ThemeContext.Provider value="dark">
<UserContext.Provider value="user">
<Child />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
function Child() {
const theme = useContext(ThemeContext); // "dark"
const user = useContext(UserContext); // "user"
return <div>{theme} - {user}</div>;
}
步骤 | 操作 | 栈内容(从底到顶) | Context 当前值 |
---|---|---|---|
进入ThemeContext.Provider | pushProvider(ThemeContext, "dark") | ["light"] | ThemeContext._currentValue = "dark" |
进入UserContext.Provider | pushProvider(UserContext, "user") | ["light", "guest"] | UserContext._currentValue = "user" |
渲染Child | 消费两个 Context 的值 | 栈保持不变 ThemeContext: "dark", UserContext: "user" | |
退出UserContext.Provider | popProvider(UserContext) | ["light"] | UserContext._currentValue = "guest" |
退出ThemeContext.Provider | popProvider(ThemeContext) | [] | ThemeContext._currentValue = "light" |