React Hook之useEffect的使用和源码分析

42 篇文章 1 订阅
11 篇文章 1 订阅

作用

副作用

前端开发中的副作用一般有:dom操作、浏览器事件的绑定和取消绑定、http请求、打印日志、访问系统状态、执行IO更新等。

在class类组件中,副作用一般写在componentDidMount,componentDidUpdate, componentWillUnMount, componentWillUpdate里,但是函数组件没有生命周期,这个时候就可以用useEffect来解决了,这一个hook可以替代以上四个生命周期;

useEffect的使用

useEffect是组件第一次渲染和每次更新,即componentDidMount,componentDidUpdate时都会执行的;

useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,

create方法是每次执行useEffect时都会执行一次

create方法可以返回一个清除方法(只能是一个普通的方法),该方法会在componentWillUnMount,componentWillUpdate时执行;
一般在这个时候清除副作用;
如果不需要清除的话,不写返回清除方法即可。

deps:依赖项组成的数组,这个参数可以控制useEffect的方法不要每次执行,只有数组里的依赖的值发生改变时再执行。
如果useEffect只需要在第一次渲染时执行一次,deps传入空数组即可;
如果deps不传,则组件每次更新时都会执行useEffect里的方法;

源码分析

由上一篇的useState和useReducer可知,useEffect也是根据组件第一次渲染和更新分别调用的不同的方法,他们分别是:mountEffect和updateEffect。

  1. 先来看一下mountEffect,第一次渲染组件时做了什么
// ReactFiberHooks.js
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}

由源码可知,mountEffect走了mountEffectImpl方法,并传入了个奇奇怪怪得东西UpdateEffect | PassiveEffectUnmountPassive | MountPassive

  1. UpdateEffect | PassiveEffectUnmountPassive | MountPassive是什么?
    答: 是一个二进制数字,用来标记是什么类型的副作用的

  2. 继续看mountEffectImpl方法

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  //往hook链表里追加一个hook
  const hook = mountWorkInProgressHook();
  // 没有传deps时,会被处理成null
  const nextDeps = deps === undefined ? null : deps;
  sideEffectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

useEffect的hook的memoizedState对象好像不太一样,把hook存到链表中以后还把pushEffect的返回值存了下来。

  1. pushEffect做了什么呢,很重要的亚子!
// 返回了一个effect对象
function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag,
    create,
    destroy, // mountEffectImpl传过来的是undefined
    deps,
    // Circular
    next: (null: any),
  };
  // 一个全局变量,在renderWithHooks里初始化一下,存储全局最新的副作用
  if (componentUpdateQueue === null) {
  	// { lastEffect: null }
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    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;
}

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

  1. 再来看看组件更新时调用updateEffect做了什么
// 跟mountEffect的执行类似,只不过走的是updateEffectImpl方法
// 直接看updateEffectImpl

function updateEffectImpl(fiberEffectTag, hookEffectTag, 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,
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
	// 两次依赖项不同,componentUpdateQueue上增加一个effect,并且更新当前hook的memoizedState值
  sideEffectTag |= fiberEffectTag;
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

这一段有个重点: useEffect的依赖项没变化的时候,componentUpdateQueue增加一个tag为NoHookEffect
= 0 的effect

  1. 那么他是怎么比较deps的依赖项有没有更新的呢?
unction areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  if (prevDeps === null) {
    
    // ... 省掉一些_Dev_处理
    return false;
  }
  // ... 再次省掉一些_Dev_处理
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

哦,其实就是遍历deps数组,对每一项执行Object.is()方法

  1. 最后,我们来看一下useEffect什么时候执行,这得从react的函数组件的生命周期相关的调度开始。

在fiber的调度过程中,最终追溯到commitBeforeMutationLifeCycles方法,在这里会根据组件类型,去执行对应的生命周期,FunctionComponent组件执行commitHookEffectList方法

function commitHookEffectList(
  unmountTag: number,
  mountTag: number,
  finishedWork: Fiber,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    // 上面说到 NoHookEffect = 0
    // 当effect.tag是0的时候,跟谁做与运算都会得到0, 即不执行任何操作
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
		// _Dev_中的一些警告处理省略掉
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

由源码可知,此时effect链表里的hook会被依次执行;
在执行的时候,会把effect.tag跟一个变量做与运算;
然后判断跟NoHookEffect(值为0)是否相等。

所以当effect.tag的值是0,不管跟谁做与运算,结果都是0 ,这个时候不会执行更新。(前面也说到,当useEffect的依赖项没更新时候,会声明一个tag是0的effect,所以这个时候,组件不会发生更新。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值