前言
在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对应的hookcurrentlyRenderingFiber.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代表单链表的最后一个hookfirstEffect代表单链表的最前面的一个hooklastEffect.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为UpdatehookFlags为Layout
而useEffect()的传入参数为
fiberFlags为PassivehookFlags为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=5valueCursor=[1, 2, 3, 4]
当加入新的元素6时,会变成
cursor.current=6valueCursor=[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" |