react hooks--useEffect
概述
- React 在构建用户界面整体遵循函数式的编程理念,即固定的输入有固定的输出,尤其是在推出函数式组件之后,更加强化了组件纯函数的理念。
- 但实际业务中编写的组件不免要产生请求数据、订阅事件、手动操作 DOM 这些副作用 effect ,这样难免让函数组件变得不那么纯,于是 React 提供 useEffect 和 useLayoutEffect 这样的 hook,给开发者提供专门管理副作用的方式。
◼ 目前我们已经通过useState hook在函数式组件中定义state并且可以对其进行状态的修改,那么类似于生命周期这些呢?
- Effect Hook 可以帮助我们来完成一些类似于class(类组件)中生命周期的功能;
- 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
- 所以对于完成这些功能的Hook被称之为 Effect Hook
◼ useEffect的解析:
- 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
- useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
- 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
语法:
useEffect(effect, [deps]);
流程概述
在 React 中,useEffect 和 useLayoutEffect 分为两个阶段: render 阶段和 commit 阶段。这两个阶段分别发生在组件的渲染过程中,以确保正确地处理副作用
- render 阶段: 函数组件开始渲染的时候,创建出对应的 hook 链表挂载到 workInProgress 的 memoizedState 上,并创建 effect 链表,但是基于上次和本次依赖项的比较结果;
- commit 阶段: 异步调度 useEffect,layout 阶段同步处理 useLayoutEffect 的 effect,也就是在浏览器进行布局 layout 和回执 paint 之前同步执行。等到 commit 阶段完成,更新应用到页面上之后,开始处理 useEffect 产生的 effect;
两者的区别是 useEffect 不会阻塞 DOM 的更新,useLayoutEffect 在 DOM 更新前同步触发,会阻塞 DOM 的更新。
调用 useEffect 的完整流程
在 React 的 hook 架构中,每个 hook 函数都会有两个生命阶段:
- mount 阶段;
- update 阶段;
mount 阶段
mount 阶段,调用 useState 和 useLayoutEffect 这个 hook,React 内部会调用 mountEffect ,代码如下图所示:

mountEffect
接下来我们看看 mountEffect 这个函数,具体代码如下所示:

首先我们来看看这个函数接收的参数是个怎么样的类型,如下图所示:

它的第一个参数就是一个 create 函数,就是我们在使用 useEffect 中传入的第一个回调函数,也就是 create 函数。而第二个参数就是我们传进去的依赖数组。
在这个函数里面也就是根据不同的类型调用不同的 mountEffectImpl 函数。
mountEffectImpl
mountEffectImpl 该函数的具体代码如下所示:

在这里的第一个参数中 fiberFlags 用于表示不同的组件状态和需要进行的操作,帮助 React 在组件树上进行高效的更新和渲染。
紧接着调用 mountWorkInProgressHook 开始创建 hook,如果它是第一次调用,它的值为 null。只有调用过一次 hook 函数后,它才不为 null。
所以这个阶段为创建 hook 阶段,接下来进入创建 effect 阶段。
值得注意的是,pushEffect 函数的第三个参数是 destroy 函数,它是 create 函数的返回值,在 mount 阶段,它被被赋值为 undefined。
当组件 mount 阶段之后,create 函数就一定会执行,而 destroy 函数通常在 create 函数的返回值中提供,用于执行副作用的清理操作。在 create 函数中返回一个函数,这个函数会在组件卸载或下一次渲染之前被调用,用于清理之前创建的副作用。
pushEffect
该函数是 React 用于管理组件中的副作用 Effects 的函数。在 React 中,副作用指的是在组件渲染时产生的一些操作,比如订阅事件、数据获取、DOM 操作等。React 使用这个函数来将副作用相关的信息组织成链表,并将其连接到组件的 Fiber 节点上。
接下来我们看看这个函数的完整代码,如下所示:

首先它接收四个参数,它们分别是:
- tag: 一个标志位,用于标识副作用的类型;
- create: 一个函数,用于执行副作用。通常是 useEffect 中传递的副作用函数;
- destroy: 一个函数,用于执行清理操作。通常是 useEffect 中传递的返回函数的清理函数;
- deps: 一个数组,表示副作用的依赖项。用于指定在哪些依赖项发生变化时,重新运行副作用函数;
该函数主要的逻辑有以下几个方面:
- 首先,根据传入的参数创建一个 Effect 对象,并设置其 tag、create、destroy 和 deps 属性;
- 获取当前渲染的组件对应的更新队列 componentUpdateQueue,它主要用于存储组件的更新状态和副作用;
- 如果当前组件的更新队列为空,说明当前组件是首次渲染,需要创建一个新的更新队列,并将 componentUpdateQueue 设置为它。同时,将 effect 添加到 componentUpdateQueue 的 lastEffect 中,形成一个单节点的链表;
- 如果当前组件的更新队列不为空,说明当前组件已经渲染过,此时需要将 effect 添加到已有的链表中。具体做法是将 effect 添加到 lastEffect 的后面,形成一个新的链表节点;
- 返回 effect;
在这里我们可以看出 componentUpdateQueue 和 effect 是存储的内容是基本一致的:

等该函数调用结束,最终又返回 mountEffectImpl 函数中,将所返回的 effect 赋值给 hook.memoizedState:

当你到这里把整个流程走完成之后,整个时候 hook 函数已经被创建起来了,在当前的 workInProgress 上已经构建完成了,我们把目光放回到一个古老函数中:

currentlyRenderingFiber 就是当前正在创建的 fiber 树,我们看看它是怎么样的结果,我们现在我们的项目中定义以下组件:

在 currentlyRenderingFiber.memorizedState 中它是一个单链表结构,对应着每一个 hook 函数:

每一个 hook 函数中的 memorizedState 又 updateQueue 中的 update 对象有相关联:

在 update 对象中 指向的是 effect 链表中的最后一个节点,而该节点就是 currentlyRenderingFiber.memorizedState 中的最后一个 hook 链表。
整个 <App /> 组件函数最终形成的数据结构有如下图所示:

那么问题来了,既然有了 memorized 存储的单向 hooks 链表,为什么 updateQueue 也存储一个循环链表?
在 Fiber 树中,memorized 存储的是一个单向 hooks 链表,而 updateQueue 存储的是一个循环链表。这样设计的原因是为了在处理异步更新和批量更新时提供更好的性能和灵活性。
首先,memorized 单向 hooks 链表用于存储组件的当前状态和数据,它记录了每个 useState 或者 useReducer 的值。这个链表的顺序是固定的,每个 hook 在链表中都有一个唯一的位置。
然而,updateQueue 循环链表则用于存储组件的更新操作。由于组件可以多次触发更新,并且每次更新可能会包含多个状态的改变,使用一个循环链表可以方便地记录和管理这些更新操作。循环链表可以按照顺序追踪更新的发生,并且可以很方便地进行添加、删除和遍历操作。
此外,循环链表的设计还符合 React 的更新机制。在 React 更新过程中,会根据更新的优先级来处理不同类型的更新。通过循环链表,React 可以按照优先级顺序依次处理更新,从而提高更新的效率。
综上所述,memorized 单向 hooks 链表用于存储组件状态和数据的快照,而 updateQueue 循环链表用于存储组件的更新操作,这两种链表的设计相互配合,提供了高效和灵活的状态管理和更新机制。
update 阶段
当组件进行更新时,会调用该函数进行更新:

而该函数实际上又是调用另外一个函数,如下图所示:

在这个函数里,我们将进入 update 阶段的真正的处理。
updateEffectImpl
该函数的具体代码如下所示:

既然有了源代码了,那么接下来我们在我们的项目中定义以下代码,如下所示:

这段代码和之前的不同的是我们定义了一个 setState 函数用于触发 useEffect 的更新,因为当状态发生变化的时候会触发 useEffect 函数的执行。
在这个例子中,当我们点击了 button 按钮的时候,就会触发 updateEffectImpl,该函数的主要流程有以下几个方面:
- 调用
updateWorkInProgressHook()取上一个渲染周期的 hook 链中找到与当前位置编号对应的旧 hook 对象并返回赋值给 hook; - 将传入的依赖数组 deps 转换成 null 或者具体的依赖数组;
- 初始化 destroy 变量,赋值为 undefined;
if(currentHook !== null)如果为 true,则表明有旧的 hook,即当前正在更新的 hook,则检查是否存在依赖项 deps 变化并检查依赖项是否为 null;- 通过 prevEffect.deps 获取之前保存的旧依赖项,并赋值给 nextDeps;

if(areHookInputsEqual(nextDeps, prevDeps))检查新旧依赖项是否相等,如果相等,则不需要重新创建副作用,直接复用旧的副作用,并返回;-

- 最后如果依赖项有变化或之前没有 hook,则将新副作用添加到 hook 中。也就是调用
hook.memoizedState = pushEffect(...)
这个函数的作用是在 React 组件更新时,根据依赖项 deps 的变化来管理 hook 函数的副作用的创建和复用。如果依赖项没有变化,将复用旧的副作用,否则创建新的副作用,并在需要时执行旧副作用的清理操作。
小结一下,在 useEffect 的 mount 阶段,useEffect 的 create 函数是一定执行的。而在 update 阶段,useEffect 阶段的 create 阶段是只有在依赖项发生了变化的时候才会发生变化才会被执行。
处理 Effect 回调
react 在 commit 阶段,它又分为三个小阶段: before mutation、mutation、layout。
其中具体操作 dom 的阶段是 mutation,操作 dom 之前是 before mutation,而操作 dom 之后是 layout。
layout 阶段在操作 dom 之后,所以这个阶段是能拿到 dom 的,ref 更新是在这个阶段,useLayoutEffect 回调函数的执行也是在这个阶段。
commitRoot
在开始之前,我们先来捋清一下这个流程,首先,整个 commit 阶段的入口函数为 commitRoot,而在该函数中调用 commitRootImpl 进入 commit 阶段:

而在该函数中,又循环调用 flushPassiveEffects 函数,如下代码所示:

这个函数主要用于处理副作用的。最终会遍历 effect 链表执行每个 effect 的 create 和 destroy 函数。并执行 flushPassiveEffectsImpl 方法,对于 flushPassiveEffectsImpl,它主要做了两件事情:
- 调用 useEffect 的销毁函数;
- 调用 useEffect 的回调函数;

这三个阶段都在这里调用,如下图所示:

Before Mutation
进入到 commitBeforeMutationEffects 函数中,这时候该函数主要的作用还是调用 commitBeforeMutationEffects_begin 函数,到了这里就是真正的处理的时候了,如下代码所示:

commitBeforeMutationEffects_begin 函数它主要用于处理事件和删除操作,并确保在进行 DOM 修改之前正确应用这些副作用。这个也就是我们之前所说的 before mutation阶段
在 Commit 阶段的不同子阶段中,React 会逐步将更新应用到实际 DOM 中,完成组件的更新和渲染过程。
其中调用 commitBeforeMutationEffects_complete() 函数表示该子阶段处理完成。这个函数主要做的事情是进入一个循环,该循环讲一直执行直到 nextEffect 为 null,也就是遍历完成所有待处理的 Fiber 节点。
调用 commitBeforeMutationEffectsOnFiber(fiber) 函数处理特定 Fiber 节点上的操作,如果出现错误,它会捕获并处理错误,然后继续处理下一个节点或兄弟节点,最终完成提交阶段的准备工作。
mutation 阶段
这是 commit 三个子阶段中的第二个阶段,也就是 mutation 阶段,在这个阶段,React 在这个阶段对 DOM 进行了更新操作。
在该函数中实际上调用的是 commitMutationEffectsOnFiber 函数:

commitMutationEffectsOnFiber 的核心实现为不同类型的 fiber 进行不同的处理,但有一些公共的逻辑会执行,它们分别是做删除和插入操作:
- recursivelyTraverseMutationEffects: 执行删除操作;
- commitReconciliationEffects: 执行插入操作;

recursivelyTraverseMutationEffects 执行删除操作
这个方法中主要做的事情是处理删除工作,它会读取 fiber 上的 deletions 数组,对要删除的 fiber 进行操作。

而该函数中,又是调用另外一个函数,这个函数就是做的一些核心操作了。

删除操作详解
进入到 commitDeletionEffectsOnFiber 函数中,说明我们进入到了真正的删除操作了,首先我们来看看这个函数大体。

它主要做的事情是跟着不同的 deletedFiber.tag 也就是不同的组件类型去做不同的删除操作。
原生组件
执行原生组件会执行两个 switch 的 case,如下代码所示:

在开始之前,我们先来了解一下 HostComponent 和 case HostText 的区别,如下代码所示:

在上述 JSX 代码中,React 将会创建一个树状结构,其中包含了以下几个节点:
- 一个 HostComponent 节点表示外层的
div元素 - 一个 HostText 节点表示文本内容
hello - 一个 HostComponent 节点表示
span元素 - 另一个 HostText 节点表示文本内容
world
对于这些组件,也就是原生组件,当 React 执行删除操作的时候,会有以下过程:
- 首先将绑定的 ref 值为 null;
- 递归对它的子 fiber 进行删除操作;
- 最终从 DOM 树中删除对应的 DOM;
类组件
对于类组件,也就是代码里面对应的 ClassComponent,具体代码如下所示:

这段代码主要的操作有以下几个方面:
- 调用 safelyDetachRef 函数,移除 ref 引用;
- 检查该类组件实例是否定义了 componentWillUnmount 生命周期方法。如果定义了,则调用 safelyCallComponentWillUnmount 函数,安全地执行 componentWillUnmount 方法;
- 递归处理 fiber 子阶段进行删除操作;
函数组件
对于类组件,也就是代码里面对应的 FunctionComponent、ForwardRef、MemoComponent、SimpleMemoComponent,具体代码如下所示:

这里主要做的事情是遍历它的 updateQueue 队列,并通过 effect 的 tag 来识别类型来决定是否调用 destroy 方法。
对 useInsertionEffect 和 useLayoutEffect,调用它们的 destroy 方法。destroy 就是执行 useInsertionEffect / useLayoutEffect 的回调函数所返回的函数。useEffect 则跳过,不调用 destroy 方法。
最后还是递归子 fiber 进行删除操作。
commitReconciliationEffects 执行插入操作
完成删除逻辑后,接着就是调用 commitReconciliationEffects,这个方法负责往真实 DOM 树中插入 DOM 节点。
完整代码如下所示:

更新操作
对于前面中说到的原生组件,后面还有一段逻辑需要执行,如下所示:

首先检查 flags 是否包含 Update 标志位,如果存在,表示当前 fiber 需要进行属性更新操作。
获取当前 HostComponent 节点的实例 instance,即对应的真实 DOM 元素是否存在,如果存在获取当前节点的新属性 newProps 和旧属性 oldProps。
然后将当前节点的实例 instance、属性更新的有效载荷 updatePayload、节点类型 type、旧属性 oldProps、新属性 newProps 和当前节点的更新队列 finishedWork 作为参数传递给 commitUpdate 函数。
该函数完整代码如下所示,很简单,也就调用两个函数:

这个函数的主要作用是在 React 的提交阶段,将更新后的属性应用到真实的 DOM 元素上,以确保虚拟 DOM 更新与实际的 DOM 同步。它执行了以下两个主要任务:
- 调用 updateProperties 函数,将属性更新应用到 DOM 阶段上:
-
- domElement 是要更新的真实 DOM 元素;
- updatePayload 是属性更新的有效载荷,其中包含了实际变化的属性信息;
- type 是 DOM 元素的类型;
- oldProps 是之前的属性;
- newProps 是更新后的属性;
- 此函数通过将 updatePayload 中的属性变化应用到 domElement 上,实现了属性的更新;

- 调用 updateFiberProps 函数,更新保存在 React Fiber 节点上的属性信息:
-
- domElement 是要更新的真实 DOM 元素;
- newProps 是更新后的属性;
- 此函数将更新后的属性信息保存在 React Fiber 节点的属性上,以便在后续的更新中使用;
总的来说,这个函数负责在提交阶段将虚拟 DOM 更新的属性变化应用到真实的 DOM 元素上,并更新 React Fiber 节点中的属性信息,以确保虚拟 DOM 和实际 DOM 之间保持同步。这是 React 在提交阶段执行的一部分,用于将虚拟 DOM 的属性变化映射到实际的 DOM 中。
layout 阶段
这个阶段进入第三阶段,也就是 DOM 变更后,它会有以下调用顺序 commitLayoutEffects -> commitLayoutEffects_begin,那么接下来我们看看该函数主要做的事情有什么,如下代码所示:

该函数省略了一个 if 判断,主要判断该组件是否属于 keep-alive 组件,但是目前还没有提供相关 API,这里暂时不做讲解。
该函数主要用于在提交阶段执行布局效果 layout effects,即处理组件布局相关的操作。它会遍历 fiber 树中的节点,根据不同的条件来执行不同的布局操作。以下是代码的主要作用:
- 遍历 fiber 树中的节点,从 nextEffect 开始,直到遍历完所有的节点;
- 如果当前 fiber 节点有子节点 firstChild 不为 null,且其 subtreeFlags 中包含布局标志 LayoutMask,则将第一个子节点的 return 指向当前 fiber,将 nextEffect 指向第一个子节点,继续遍历子树的布局效果;
- 如果当前 fiber 节点没有子节点或者没有布局标志,执行 commitLayoutMountEffects_complete 函数,完成该节点的布局操作;
commitLayoutMountEffects_complete 函数主要做的事情就是对于每个 fiber 节点,检查其 flags 是否包含布局标志 LayoutMask:
- 如果包含,表示该节点需要执行布局效果,即进行组件的布局操作;
- 获取当前节点的备份 current,用于比较和更新布局效果;
- 调用 commitLayoutEffectOnFiber 函数,执行当前节点的布局效果操作;
- 在布局操作期间,可能会捕获并处理布局阶段的错误
在 commitLayoutEffectOnFiber 函数中依然是根据不同的组件类型调用不同的方法,先来看看类组件的:

会在这个阶段调用 componentDidMount 生命周期方法。
在函数组件中,会调用 commitHookEffectListMount 函数,该函数主要实现如下所示:

该函数的主要用于组件挂载阶段执行 hook effects 的操作。它会遍历 Hook 链表,对每个 Hook 效果进行相应的操作,包括创建和执行清理操作。以下是这个函数的主要作用:
- 获取 finishedWork 中的更新队列 updateQueue,该队列保存了组件的 Hook 效果;

- 从更新队列中获取最后一个 Hook 效果 lastEffect,并从中获得第一个 Hook 效果 firstEffect;
- 循环遍历每个 Hook 效果,检查其标志 tag 是否与传入的 flags 匹配,即是否需要执行当前操作;
- 如果需要执行当前操作,则根据 Hook 的类型执行相应的操作:
-
- 如果是被标记为 HookPassive 的效果,表示 passively 进行的 Hook 效果,会进行 passively 渲染。
PassiveEffect 是一种副作用标志,用于表示在组件的生命周期中执行的副作用类型。副作用是指在组件渲染期间可能执行的操作,例如订阅、取消订阅、数据获取等。而 PassiveEffect 表示一种被动的、不会触发组件重新渲染的副作用。这意味着在执行这种副作用时,React 不会因为副作用的执行而重新渲染组件。这对于性能优化非常有用,因为它允许开发人员在不影响渲染性能的情况下执行副作用操作。
- 如果是被标记为 HookLayout 的效果,表示布局相关的 Hook 效果,会执行布局操作;
- 调用 create() 创建 Hook 效果,并将返回的清理函数存储到 effect.destroy 中,这个
create()函数也就是我们使用useEffect中传入的第一个回调函数了,会在这个阶段执行。
- 循环遍历完所有的 Hook 效果后,完成组件的挂载阶段的 Hook 效果操作;
这个函数在组件挂载阶段负责执行 Hook 效果的相关操作,包括创建效果和执行清理操作。这是 React Hooks 在组件生命周期中的一部分,用于处理 Hook 效果的创建、执行和清理工作,确保 Hook 效果在组件挂载期间正确地工作。
useEffect
现在我们再回到 useEffect 这个 hook,它不在同步的 commit 阶段中执行,它是异步的,被 Scheduler 异步调度了,如下代码所示:

在这个函数里,先执行所有 useEffect 的 destroy 方法,然后才执行所有 useEffect 的 create 方法。并保持顺序是先子后父。

所以整个 commit 阶段有如下流程图所示:

side effect - 副作用
使用场景:当你想要在函数组件中,处理副作用(side effect)时,就要使用 useEffect Hook 了
作用:处理函数组件中的副作用(side effect)
问题:副作用(side effect)是什么?
回答:在计算机科学中,如果一个函数或其他操作修改了其局部环境之外的状态变量值,那么它就被称为有副作用
类比,对于 999 感冒灵感冒药来说:
- (主)作用:用于感冒引起的头痛,发热,鼻塞,流涕,咽痛等
- 副作用:可见困倦、嗜睡、口渴、虚弱感
理解:副作用是相对于主作用来说的,一个功能(比如,函数)除了主作用,其他的作用就是副作用
对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM)
React 组件的公式:UI = f(state)
常见的副作用(side effect)
- 数据(Ajax)请求、手动修改 DOM、localStorage 操作等
作用:
- 让函数组件可以模拟生命周期
- useEffect可以模拟componentDidMount、componentDidUpdate、componentWillUnmount等等。相当于生命周期的结合体
- useEffect有两个参数:
useEffect(effect, [deps]);
-
- 第一个参数effect是一个函数,这是一个副作用函数,可以在这个函数中执行副作用操作,比如:改变 DOM、添加订阅、设置定时器、记录日志、发送请求获取后端数据等等。
- 第二个参数是一个数组,数组里面可以填写你要监听的值,当这个值改变后,effect函数就会重新运行。
基本使用
使用场景:当你想要在函数组件中,处理副作用(side effect)时,就要使用 useEffect Hook 了
作用:处理函数组件中的副作用(side effect)
注意:在实际开发中,副作用是不可避免的。因此,react 专门提供了 useEffect Hook 来处理函数组件中的副作用
import { useEffect } from 'react'
useEffect(function effect() {
document.title = `当前已点击 ${count} 次`
})
useEffect(() => {
document.title = `当前已点击 ${count} 次`
})
解释:
- 参数:回调函数(称为 effect),就是在该函数中写副作用代码
- 执行时机:该 effect 会在组件渲染后以及组件更新后执行
- 相当于componentDidMount + componentDidUpdate
模拟生命周期
componentDidUpdate
- 问题:如果组件中有另外一个状态,另一个状态更新时,刚刚的 effect 回调,也会执行
- 性能优化:跳过不必要的执行,只在 count 变化时,才执行相应的 effect
useEffect(() => {
document.title = `当前已点击 ${count} 次`
}, [count])
解释:
- 第二个参数:可选的,可省略;也可以传一个数组,数组中的元素可以成为依赖项(deps)
- 该示例中表示:只有当 count 改变时,才会重新执行该 effect
App.js
import React from 'react'
import UseEffect from './components/UseEffect'
export default function App() {
let msg = '132'
return (
<div>
<UseEffect />
</div>
)
}
components/UseEffect.jsx
import React from "react";
import { useEffect, useState } from "react";
export default function UseEffect() {
let [age, setAge] = useState(20);
// 注意:模拟componentDidUpdate,第二个参数不传递,每次组件更新渲染后都会执行effect函数
useEffect(() => {
console.log('无数组, 模拟omponentDidUpdate')
})
//监听
useEffect(() => {
console.log("监听数据,模拟componentDidUpdate");
}, [age]);
const changeAge = () => {
setAge(21)
}
return (
<div>
<p>{age}</p>
<button onClick={changeAge}>更改age</button>
</div>
);
}
componentDidMount
useEffect 的依赖是一个空数组
useEffect 的第二个参数,还可以是一个空数组([]),表示只在组件第一次渲染后执行 effect
使用场景:1 事件绑定 2 发送请求获取数据 等
useEffect(() => {
const handleResize = () => {}
window.addEventListener('resize', handleResize)
}, [])
解释:
- 该 effect 只会在组件第一次渲染后执行,因此,可以执行像事件绑定等只需要执行一次的操作
-
- 此时,相当于 class 组件的 componentDidMount 钩子函数的作用
- 跟 useState Hook 一样,一个组件中也可以调用 useEffect Hook 多次
- 推荐:一个 useEffect 只处理一个功能,有多个功能时,使用多次 useEffect
App.js
import React from 'react'
import UseEffect from './components/UseEffect'
export default function App() {
let msg = '132'
return (
<div>
<UseEffect />
</div>
)
}
components/UseEffect.jsx
import React from "react";
import { useEffect, useState } from "react";
export default function UseEffect() {
let [age, setAge] = useState(20);
useEffect(() => {
console.log("空数组, 模拟componentDidMount生命周期");
}, []);
useEffect(() => {
console.log("监听数据,模拟componentDidUpdate");
}, [age]);
const changeAge = () => {
setAge(21);
};
return (
<div>
<p>{age}</p>
<button onClick={changeAge}>更改age</button>
</div>
);
}
componentWillUnmount
useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B

◼ 为什么要在 effect 中返回一个函数?
这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
如此可以将添加和移除订阅的逻辑放在一起;
它们都属于 effect 的一部分;
◼ React 何时清除 effect?
React 会在组件更新和卸载的时候执行清除操作;
正如之前学到的,effect 在每次渲染的时候都会执行;
注意:模拟componentWillUnmount,在effect函数中返回一个函数,这个函数会在组件卸载时执行
App.js
import React from 'react'
import UseEffect from './components/UseEffect'
import {useState} from 'react'
export default function App() {
let [flag, setFlag] = useState(true)
return (
<div>
{
flag ? <UseEffect />: <React.Fragment></React.Fragment>
}
<button onClick={() => setFlag(false)}>修改flag</button>
</div>
)
}
components/UseEffect.jsx
import React from "react";
import { useEffect, useState } from "react";
export default function UseEffect() {
let [age, setAge] = useState(20);
// componentDidMount
useEffect(() => {
console.log("空数组, 模拟componentDidMount生命周期");
}, []);
// componentDidUpdate
// 注意:模拟componentDidUpdate,第二个参数不传递,每次组件更新渲染后都会执行effect函数
useEffect(() => {
console.log("无数组, 模拟omponentDidUpdate");
});
useEffect(() => {
console.log("监听数据,模拟componentDidUpdate");
}, [age]);
const changeAge = () => {
setAge(21);
};
// componentWillUnmount
++ useEffect(() => {
监听:
eventBus事件
定时器
...
++ return () => {
// 取消事件监听
++ console.log("模拟componentWillUnmount");
++ };
++ }, []);
return (
<div>
<p>{age}</p>
<button onClick={changeAge}>更改age</button>
</div>
);
}
useEffect(() => {
// 进行事件监听
const unubscribe = store.subscribe(() => {
// ...
})
function foo() {}
eventBus.on('xxx', foo)
// 取消监听--清理副作用
// 返回值:回调函数,会在组件被重新渲染或者组件卸载的时候执行
return () => {
// 取消监听
}
})
清理副作用
有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
还有一些副作用是需要清除的:
- 例如订阅外部数据源, 开启定时器,注册事件。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!
- 比如我们之前的事件总线或Redux中手动调用subscribe,都需要在componentWillUnmount有对应的取消订阅
问题:如何在组件卸载时,解绑事件?此时,就用到 effect 的返回值了
useEffect(() => {
const handleResize = () => {}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
解释:
- effect 的返回值也是可选的,可省略。也可以返回一个清理函数,用来执行事件解绑等清理操作
- 清理函数的执行时机:1 组件卸载时 2 effect 重新执行前
-
- 此时,相当于 class 组件的 componentWillUnmount 钩子函数的作用
- 推荐:一个 useEffect 只处理一个功能,有多个功能时,使用多次 useEffect
useEffect(() => {
// 监听subscribe
const unubscribe = store.subscribe(() => {
// ...
})
// 取消监听--清理副作用
// 返回值:回调函数,会在组件被重新渲染或者组件卸载的时候执行
return () => {
// 取消监听逻辑
}
})
useEffect(() => {
// 监听 eventBus
function foo() {}
eventBus.on('xxx', foo)
// 取消监听eventBus
return () => {
// 取消监听逻辑
}
})
// ......
- 优势:根据业务逻辑来拆分,相同功能的业务逻辑放在一起,而不是根据生命周期方法名称来拆分代码
- 编写代码时,关注点集中;而不是上下翻滚来查看代码
将事件处理程序放在 useEffect 内部:
// 1 将 resize 事件处理程序放在 effect 回调中,当前这个代码是没有问题的
useEffect(() => {
const handleResize = () => {
console.log('window 窗口大小改变了')
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
// 2 将 resize 事件处理程序拿到 useEffect 的外部,当前这个代码是没有问题的
const handleResize = () => {
console.log('window 窗口大小改变了')
}
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
// 3 有依赖项的情况:
useEffect(() => {
// resize 事件的处理程序
const handleResize = () => {
console.log('window 窗口大小改变了', count)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [count])
// 注意:此处的代码,会给一些警告!!! 不要按照这种方式写代码!!!
// 4 如果将 handleResize 放到了 useEffect 外部,React 会给以警告:
// 要么将 handleResize 放到 useEffect 中
// 要么使用 useCallback 这个 hook 来包裹 handleResize
// resize 事件的处理程序
const handleResize = () => {
console.log('window 窗口大小改变了', count)
}
useEffect(() => {
console.log('useeffect 执行了')
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [handleResize])
// 总结以上几种情况,推荐:在给 window 绑定事件时,将 事件处理程序放在 useEffect 内部。
useEffect 发送请求:
在组件中,使用 useEffect Hook 发送请求获取数据(side effect):
useEffect(() => {
const loadData = async () => {}
loadData()
}, [])
// https://github.com/facebook/react/issues/14326#issuecomment-441680293
useEffect(() => {
// 是否取消本次请求
let didCancel = false
async function fetchMyAPI() {
let url = 'http://something/' + productId
let config = {}
const response = await myFetch(url)
// 如果开启其他请求,就忽略本次(过时)的请求结果
if (!didCancel) {
console.log(response)
}
}
fetchMyAPI()
return () => { didCancel = true } // 取消本次请求
}, [productId])
解释:
- 注意:effect 只能是一个同步函数,不能使用 async
- 因为 effect 的返回值应该是一个清理函数,React 会在组件卸载或者 effect 的依赖项变化时重新执行
- 但如果 effect 是 async 的,此时返回值是 Promise 对象。这样的话,就无法保证清理函数被立即调用
- 如果延迟调用清理函数,也就没有机会忽略过时的请求结果或取消请求
- 为了使用 async/await 语法,可以在 effect 内部创建 async 函数,并调用
// 错误演示:
// 不要给 effect 添加 async
useEffect(async () => {}, [])
useEffect循环报错问题:
useEffect(() => {
const getList = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
setList(res.data.data.channels)
}
console.log(list)
getList()
}, [list])
可以在模拟componentWillUnmount卸载阶段的函数中去做资源的清理工作,比如清理定时器,订阅发布等等
let timer = null;
useEffect(() => {
console.log('空数组, 模拟componentDidMount生命周期')
// 在componentDidMount中设置定时器
timer = setInterval(() => {
console.log(1)
}, 1000);
}, [])
useEffect(() => {
return () => {
console.log('模拟componentWillUnmount')
// 清理定时器
clearInterval(timer)
}
}, []);
不要对useEffect的依赖项撒谎:
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = '点击了' + count + '次'
}, [])
return (
<div>
<h1>计数器:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<hr />
</div>
)
}
useEffect完全指南:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
Effect性能优化
◼ 默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
另外,多次执行也会导致一定的性能问题;
◼ 我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?
useEffect实际上有两个参数:
参数一:执行的回调函数;
参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
◼ 但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:
那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;
useEffect(() => {
// 监听subscribe
const unubscribe = store.subscribe(() => {
// ...
}, [])
// 取消监听--清理副作用
// 返回值:回调函数,会在组件被重新渲染或者组件卸载的时候执行
return () => {
// 取消监听逻辑
}
}, [])
useEffect(() => {
// 监听 eventBus
function foo() {}
eventBus.on('xxx', foo)
// 取消监听eventBus
return () => {
// 取消监听逻辑
}
}, [])
useEffect(() => {
// 监听 counter 的改变
console.log('counter的变化', counter)
// 取消监听eventBus
return () => {
// 取消监听逻辑
}
}, [counter])
总结


更多推荐
所有评论(0)