背景
写这篇文章是因为工作上不是非常繁忙,可以抽空学习自己常用框架和类库,深入理解它们,在技术上希望有更大的进步,培养学习兴趣;
useEffect
和其它hooks一样,加载和更新执行不一样的方法(mountEffect和updateEffect);
1.mountEffect
页面加载时,执行mountEffect;
- 创建hook对象,加入组件的hook单向链表;
- 在组件的fiber的flag中加入副作用相关的effectTag;(加载期间默认有layoutEffect和effect的副作用)
- 创建effect对象,给hook对象的memoizedState和加入组件fiber的updateQueue中形成effect环状链表;在渲染工作完成后,会循环这个环状链表,执行每个effect对象的destory和create;
const effect = { tag, create, destroy, deps, next: null };
tag 是effect的类型 tag为9是useEffect, 5是useLayoutEffect
create是 useEffect或useLayoutEffect的回调函数
destroy是 create返回的回调函数
deps是useEffect或useLayoutEffect的依赖数组
next指向下个effect对象;
1.1.effect环状链表图
function pushEffect(tag, create, destroy, deps){
const effect = { tag, create, destroy, deps, next: null };
//新创建的effect对象为最后为effect链表的一个effect对象,componentUpdateQueue.lastEffect会指向新创建的effect对象
//新创建的effect对象的next会指向第一个effct对象;
let componentUpdateQueue = (currentlyRenderingFiber.updateQueue);
if(componentUpdateQueue === null){
//当前没有updateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
//创建updateQueue
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
//形成一个环状链表
componentUpdateQueue.lastEffect = effect.next = effect
}else{
const lastEffect = componentUpdateQueue.lastEffect;
if(lastEffect === null){
componentUpdateQueue.lastEffect = effect.next = effect;
}else{
//第一个effect对象为最先创建的的effect对象
const firstEffect = lastEffect.next; //获取第一个effect对象
lastEffect.next = effect;// 旧的最后一个effect对象的next,指向新创建的effect
effect.next = firstEffect;// 新创建的effect对象的next指向第一个effect
componentUpdateQueue.lastEffect = effect; // updateQueue的lastEffect指向effect,新创建的effect变为最后一个effect对象
}
}
return effect;
}
2.updateEffect
页面更新时,执行updateEffect;
-
根据hook单向链表获取对应的更新时的hook对象,创建新的hook对象,加入hook单向链表;
-
如果effect的deps不为null,或者undefined,会从当前hook对象拿到上一次effect对象,再从effect对象拿到deps和destroy,用新的deps与之比较;
- 如果新老deps相等,push一个不带HookHasEffect的tag给effect对象,加入updateQueue环状链表(这个effect不会被标记为有副作用,所以,effect的create和destroy不会被执行),不更新hook.memoizedState;
- 如果新老deps不相等,更新effect对象,在effect的tag中加入HookHasEffect和上一次create执行的destroy,更新hook.memoizedState;
3.useEffct的回调函数和销毁函数的执行时机
在render时期构建effect链表;在commit时执行先执行之前没有执行完的useEffect,然后,在beforeMutation阶段操作dom前,以NormalPriority常规优先级添加一个异步任务到任务队列(这个异步任务是用来执行useEffect的destroy和create的),在layout阶段完成,页面完成渲染后,执行在beforeMutation阶段添加的异步任务;
3.1.commit开始时
主要是为了执行之前没有执行的useEffect
进入commit阶段,这和useEffect异步调度的特点有关,它以一般的优先级被调度,意味着一旦有更高优先级的任务进入到commit阶段,上一次任务的useEffect还没得到执行。所以在本次更新开始前,需要先将之前的useEffect都执行掉,以保证本次调度的useEffect都是本次更新产生的。
function commitRootImpl(root, recoverableErrors, renderPriorityLevel) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
...省略代码
}
3.2.beforeMutation
只会发起一次useEffect调度,是异步调度,以NormalPriority常规优先级添加一个异步任务在任务队列中(push(timerQueue, newTask)),在页面渲染完成时,会执行这个异步任务
function commitRootImpl(root, recoverableErrors, renderPriorityLevel) {
...省略代码
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback$1(NormalPriority, function () {
//添加一个异步任务到任务队列
flushPassiveEffects(); // This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
}
...省略代码
}
3.3.layout
加载时,只执行useEffect的create函数即可;
如果pendingPassiveEffectsLanes是同步赛道,就在页面渲染完直接执行useEffect的create和destroy,在beforeMutation时添加的异步任务,不会执行useEffect的create和destory
if (includesSomeLane(pendingPassiveEffectsLanes, SyncLane) && root.tag !== LegacyRoot) {
//加载期间默认是不走这里的
//这里也是执行useEffect的create,如果pendingPassiveEffectsLanes是同步赛道,
//就在渲染完成后直接执行useEffect的create和destory
//在beforeMutation时添加的异步任务执行时,不会执行useEffect的create和destory
flushPassiveEffects();
}
执行上一次useEffect的create返回的destroy,拿到函数组件fiber的updateQueue,循环这个effect环状链表,拿到effect对象的destroy执行;
function commitHookEffectListUnmount(flags, finishedWork, nearestMountedAncestor) {
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) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
{
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectUnmountStarted(finishedWork);
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectUnmountStarted(finishedWork);
}
}
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);//执行destroy
{
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectUnmountStopped();
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectUnmountStopped();
}
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
执行完所有组件的destroy,再执行create;同理,也是拿到函数组件fiber的updateQueue,循环这个effect环状链表,拿到effect对象的create执行,然后把create返回的destroy给effect对象(留着下着更新执行useEffect时用);
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) {
{
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectMountStarted(finishedWork);
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectMountStarted(finishedWork);
}
} // Mount
var create = effect.create;
effect.destroy = create();
{
if ((flags & Passive$1) !== NoFlags$1) {
markComponentPassiveEffectMountStopped();
} else if ((flags & Layout) !== NoFlags$1) {
markComponentLayoutEffectMountStopped();
}
}
{
var destroy = effect.destroy;
if (destroy !== undefined && typeof destroy !== 'function') {
var hookName = void 0;
if ((effect.tag & Layout) !== NoFlags) {
hookName = 'useLayoutEffect';
} else if ((effect.tag & Insertion) !== NoFlags) {
hookName = 'useInsertionEffect';
} else {
hookName = 'useEffect';
}
var addendum = void 0;
if (destroy === null) {
addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
} else if (typeof destroy.then === 'function') {
addendum = '\n\nIt looks like you wrote ' + hookName + '(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + hookName + '(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
} else {
addendum = ' You returned: ' + destroy;
}
error('%s must not return anything besides a function, ' + 'which is used for clean-up.%s', hookName, addendum);
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
3.4.useEffect执行图
4.思考的问题
4.1.mountEffect和updateEffect的不同
- mountEffect会默认给effect对象的tag加入HookHasEffect;而updateEffect要判断新老deps是否相同,在依赖有变时,才会给effect对象的tag加入HookHasEffect;
- mountEffect时,effect的destroy为undefined;而updateEffect的destroy为上一次create执行时的返回值;
- mountEffect是创建新的hook对象;而updateEffect依据上一次hook对象,创建新的hook对象;
- mountEffect的hook.memoizedState是新的;而updateEffect要在依赖更新时,会更新hook.memoizedState,依赖不更新时,hook.memoizedState不更新;
4.2.effect链表会保存在hook.memoizedState和fiber.updateQueue
在hook.memoizedState时就是记录当前effect的状态,方便下一次updateEffect时,获取上一次effect的状态(会用到上一次effect的deps,destroy等);
在updateQueue时,在commit阶段处理,判断effect.tag,执行effect.destroy和effect.create;
4.3.为什么要所有(组件)的销毁函数执行完,才能执行所有的回调函数?
有的变量或者ref是多个组件共用的;如果a组件的销毁函数改变了ref.current,而b和c组件的回调函数需要用到ref.current,如果a组件的销毁函数早于b组件的回调函数,又晚于c组件的回调函数,执行的结果就不统一,所以,所有的销毁函数要早于所有的回调函数执行;(在某个组件useEffect的销毁函数中修改的ref.current可能影响另一个组件useEffect的回调函数中的同一个ref的current属性。在useLayoutEffect中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。)
useLayoutEffect和useEffect的相同点与不同点
1.相同点
- 在render阶段,都要创建effect对象和hook对象,加入到hook.memoizedState和fiber.updateQueue形成环状链表;
- 在commit阶段,destroy和create的执行方式是一样的,都是循环fiber.updateQueue,拿到effect对象的destroy和create执行,先执行所有组件的destroy,然后执行create;
2.不同点
- 在render阶段,创建的effect对象的tag属性值不同,用来区分useEffect和useLayoutEffect;
- 在commit阶段:
- 执行的时机不同,useLayoutEffect的destroy是在commitMutationEffects中,组件的dom操作完,commitWork中执行;create在commitLayoutEffects中执行;而useEffect的destroy和create执行要晚于useLayoutEffect,在页面渲染完成后执行;
- useLayoutEffect均是同步操作,执行destroy会阻塞页面渲染,useEffect有可能异步,可能同步,不会阻塞页面渲染;
3.useLayout执行图
render阶段与useEffect相同
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
{
if ( !offscreenSubtreeWasHidden) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if ( finishedWork.mode & ProfileMode) {
try {
startLayoutEffectTimer();
//执行所有的useLayoutEffect的create
commitHookEffectListMount(Layout | HasEffect, finishedWork);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
}
}
}
...
总结
1.学习方法
- 查阅网上的相关文章,对原理有了一个大概的理解;
- 写了一个dome,进行多次调试debug;
- 专注于当下,当前学的是useEffect的原理,但源码上会有很多与这次学习无关的代码,需要把它们忽略掉,这样不会浪费额外的精力;
- 一遍学不会,就第二遍,学习也是一个循环的过程;书读百遍其义自见,总能学会;不可丢失学习的热情与信心;
- 可能流程太长,很难理解,可以分解成单个函数甚至单行代码,把知识分细一点,我们一点点攻克它;