React深入useEffect

前端大神之路

共 11545字,需浏览 24分钟

 · 2021-06-01

本文适合熟悉React、以及在用useEffect遇到难题的小伙伴进行阅读。

欢迎关注前端早茶,与广东靓仔携手共同进阶~

作者:广东靓仔

一、前言

本文基于开源项目:

https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js

    最近有好友问广东靓仔:怎么写文章频率降低了?
    今年广东靓仔报名了软考,业余把精力更多投入到复习中。由于疫情影响,今天广州区暂停了软考上半年的相关科目,广东靓仔又来写文章了。
    广东靓仔将从三个方面来梳理useEffect相关内容:
    1、useEffect介绍
    2、useEffect原理
    3、useEffect源码解析    
相信有不少小伙伴在使用useEffect过程中遇到过不少问题,广东靓仔找来了几个有bug的例子:
例子一:
// 弹框显示触发定时器
useEffect(() => {
  timer = setInterval(() => {
    if (showModal) {
      requestFun()
    }
  }, 1000)
}, [showModal])  
// 关闭弹框,清除定时器
const closeModal = () => {
  clearInterval(timer)
}
弹框显示定时器开始执行,当关闭弹框。
定时器居然没有清除,有bug
例子二:
useEffect(() => {
  let intervalId = setInterval(() => {
    fetchData();
  }, 1000 * 60);
  return () => {
    clearInterval(intervalId);
    intervalId = null;
  }
}, [])
const fetchData = () => {
   request({params}).then(ret => {
      if (ret.code === OK) {
          applyResult(ret.data);
      }
   })
}
经过一番操作后
引用的值会调用上一次渲染的值,这是不对的
例子三:
当我们在useEffect调用第三方库的实例,然后在其他函数清除这个实例,发现无法清除。
其他的小伙伴在用useEffect还遇到过其他的问题,这里就不一一展开,阅读完这篇文章后,一定会对useEffect有一个全面的理解。

二、useEffect介绍

     React16.8版本中描述了在 React 渲染阶段,改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作是不被允许的,因为可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
    因此在使用useEffect完成副作用操作,赋值给useEffect的函数会在组件渲染到屏幕之后执行。useEffect一般是在每轮渲染结束后执行,当然我们也可以让它在只有某些值改变的时候才执行。
useEffect有个清除函数,官方demo如下:
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});
一般在执行一些计时器或者订阅,我们会在组件卸载后,会清除这些内容。因此可以在清除函数里面做这些操作。
useEffect为防止内存泄漏,一般情况下如果组件多次渲染,在执行下一个effect 之前,上一个 effect 就已被清除。也就是说组件的每一次更新都会创建新的订阅。
useEffect 的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。
我们都知道一旦 effect 的依赖发生变化,它就会被重新创建,例如:
useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);
 useEffect传递第二个参数,它是 effect 所依赖的值数组。只有当依赖改变后才会重新创建订阅。
温馨提示:有很多小伙伴在日常项目开发的时候,使用这个依赖的时候,很容易留下bug。比如:一个编辑弹框功能,如果useEffect依赖只写了个id,这个时候如果是对同一条数据进行编辑是不会再次执行useEffect的逻辑的。

三、useEffect原理

useEffect实际上是ReactCurrentDispatcher.current.useEffect(源码解析会讲到)

useEffect原理可以简单理解为:

  • 函数组件在挂载阶段会执行MountEffect,维护hook的链表,同时专门维护一个effect的链表。
  • 在组件更新阶段,会执行UpdateEffect,判断deps有没有更新,如果依赖项更新了,就执行useEffect里操作,没有就给这个effect标记一下NoHookEffect,跳过执行,去下一个useEffect。


我们都知道useEffect 在依赖变化时,执行回调函数。这个变化是指本次 render 和上次 render 时的依赖之间的比较。

默认情况下,effect 会在每轮组件渲染完成后执行,而且effect 触发后会把清除函数暂存起来,等下一次 effect 触发时执行,大概过程如下:


温馨提示:使用 hooks 要避免 if、for 等的嵌套使用

四、useEffrct源码解析

在react源码中,我们找到react.js中如下代码,篇幅有限,广东靓仔进行了简化,方便小伙伴阅读:

4.1 useEffect引入与导出

import {
  ...
  useEffect,
  ...
from './ReactHooks';
// ReactHooks.js
export function useEffect(
  create: (
) => (() => void) | void,
  depsArray<mixed> | void | null,
): void 
{
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  if (__DEV__) {
    if (dispatcher === null) {
     // React版本不对或者Hook使用有误什么的就报错...
    }
  }

  return ((dispatcher: any): Dispatcher);
}

上面的代码就是引入与导出过程,不难看出useEffect实际上是ReactCurrentDispatcher.current.useEffect橙色的代码

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
  current: (nullnull | Dispatcher),
};
export default ReactCurrentDispatcher;

current的类型是null或者Dispatcher,不难看出接下来我们要找类型定义

// ReactInternalTypes.js
export type Dispatcher = {|
  useEffect(
    create: () => (() => void) | void,
    depsArray<mixed> | void | null,
  ): void,
|};

4.2 组件加载调用mountEffect

函数组件加载时,useEffect会调用mountEffect,接下来我们来看看mountEffect

// ReactFiberHooks.new.js
function mountEffect(
  create: (
) => (() => void) | void,
  depsArray<mixed> | void | null,
): void 
{
    return mountEffectImpl(
      PassiveEffect | PassiveStaticEffect,
      HookPassive,
      create,
      deps,
    );
  }

PassiveEffectPassiveStaticEffect是二进制常数,用位运算的方式操作,用来标记是什么类型的副作用的。mountEffect走了mountEffectImpl方法

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

上面代码中,往hook链表里追加一个hook,把hook存到链表中以后还把pushEffect的返回值存了下来。

function pushEffect(tag, create, destroy, deps{
  const effect: Effect = {
    tag,
    create,
    destroy, // mountEffectImpl传过来的是undefined
    deps,
    next: (null: any),
  };
  // 一个全局变量,在renderWithHooks里初始化一下,存储全局最新的副作用
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 维护了一个副作用的链表,还是环形链表
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 最后一个副作用的next指针指向了自身
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

最后返回了一个effect对象。

Tips: mountEffect就是把useEffect加入了hook链表中,并且单独维护了一个useEffect的链表。

4.3 组件更新时调用updateEffect

函数组件加载时,useEffect会调用updateEffect,接下来我们来看看updateEffect

function updateEffect(
  create: (
) => (() => void) | void,
  depsArray<mixed> | void | null,
): void 
{
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 获取当前正在工作的hook
  const hook = updateWorkInProgressHook();
  // 最新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    // 上一次的hook的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比较依赖项是否发生变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果两次依赖项相同,componentUpdateQueue增加一个tag为NoHookEffect = 0 的effect,
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 两次依赖项不同,componentUpdateQueue上增加一个effect,并且更新当前hook的memoizedState值
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

从上面代码中我们看到areHookInputsEqual用来比较依赖项是否发生变化。下面我们看看这个areHookInputsEqual函数

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
{
 
  if (prevDeps === null) {
    ...
    return false;
  }

  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

上面代码中,广东靓仔删掉了一些dev处理的代码,不影响阅读。

其实就是遍历deps数组,对每一项执行Object.is()方法,判断两个值是否为同一个值。

以上内容是源码中的一部分,如果感兴趣的小伙伴可以到react仓库进行阅读~

五、总结

    在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。
    这里广东靓仔给下一些小建议:
  • 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
  • 阅读下框架官方开发人员写的相关文章
  • 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
  • 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍

关注我,一起携手进阶

如果这篇文章有触动到你,欢迎关注前端早茶,与广东靓仔携手共同进阶~


浏览 125
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报