useEffect

115 阅读4分钟

什么是 useEffect

useEffect 是执行副作用的 hook,执行时机是在页面绘制完成后,在代码中即提交阶段结束后。它接收两个参数,effect 回掉函数和 deps 依赖项。

  • effect 回调:在渲染完成、画面更新到 DOM 之后执行。

    • 可以在这里做数据请求、订阅事件、手动操作 DOM、注册定时器等。
    • 如果 effect 返回一个函数,则该返回函数被视为“清理函数”(cleanup),会在组件卸载或下一次执行新的 effect 之前调用,用来释放资源/取消订阅/清除定时器等。
  • deps 依赖数组(可选):

    • 不传:每次渲染后都执行 effect。
    • 空数组 [] :仅在首次挂载后执行一次,相当于 componentDidMount
    • [a, b, c] :只有当其中某个依赖值发生变化时,才会重新执行 effect,相当于“有条件地”模拟 componentDidUpdate
import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setData);
  }, [userId]); // 只有 userId 变化时才重新请求

}

实现

数据结构图:

截屏2025-07-30 15.40.54.png

在代码中,维护了一个 effect 循环链表的数据结构,每个 effect 和 useEffect 一一对应,并且在当前渲染 fiber 上新增一个 updateQueue 字段,这个字段中的 lastEffect 字段指向 effect 循环链表中的最后一个 effect。

代码实现

首先需要新增一个 hook。

// 省略导出新增,删除其他 hook 相关代码
const HooksDispatcherOnMount = {
  useEffect: mountEffect,
};

const HooksDispatcherOnUpdate = {
  useEffect: updateEffect,
};

对于 hook 开发,会有一个套路,要新增一个 hook 需要配套 mount 阶段执行函数和 update 阶段执行函数。

import { Passive as PassiveEffect } from "./ReactFiberFlags";
import { HasEffect as HookHasEffect, Passive as HookPassive } from "./ReactHookEffectTags";
function mountEffect(create, deps) {
  // 省略日志等代码
  return mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}

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

/**
 * 添加 effect 链表
 * @param {*} tag effect 的标签
 * @param {*} create 创建方法
 * @param {*} destroy 销毁方法
 * @param {*} deps 依赖数组
 */
function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = componentUpdateQueue.lastEffect;
      if (lastEffect === null) {
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        const firstEffect = lastEffect.next;
        lastEffect.next = effect;
        effect.next = firstEffect;
        componentUpdateQueue.lastEffect = effect;
      }
    }
  }

  return effect;
}

在 mount 阶段,完成了对上图中数据结构的构建。接下来需要消费这个数据结构。

useEddect hook 的执行时机是在页面绘制完成后,那么页面绘制是在提交阶段,消费 effect 数据结构的地方也是在提交阶段。

const { finishedWork } = root;
  if ((finishedWork.subtreeFlags & Passive) !== NoFlags || (finishedWork.flags & Passive) !== NoFlags) {
    if (!rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = true;
      scheduleCallback(flashPassiveEffect);
    }
  }

在提交阶段,根据根 fiber 的 flags 和 subtreeFlags 字段,判断当前 fiber 和 子 fiber 是否存在副作用,如果存在则执行添加一个计划,执行 flashPassiveEffect 函数,并将 rootDoesHavePassiveEffect 变量置为 true,这个变量表示当前根 fiber 存在副作用需要执行,在 DOM 变更执行完成后,重新初始化。

function flashPassiveEffect() {
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    // 执行卸载副作用
    commitPassiveUnmountEffects(root.current);
    // 执行挂载副作用
    commitPassiveMountEffects(root, root.current);
  }
}


export function commitPassiveUnmountEffects(finishedWork) {
  commitPassiveUnmountOnFiber(finishedWork);
}

function commitPassiveUnmountOnFiber(finishedWork) {
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case HostRoot: {
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      break;
    }
    case FunctionComponent: {
      recursivelyTraversePassiveUnmountEffects(finishedWork);
      if (flags & Passive) {
        commitHookPassiveUnmountEffects(finishedWork, HookPassive | HookHasEffect);
      }
    }
  }
}

function recursivelyTraversePassiveUnmountEffects(parentFiber) {
  if (parentFiber.subtreeFlags & Passive) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveUnmountOnFiber(child);
      child = child.sibling;
    }
  }
}

function commitHookPassiveUnmountEffects(finishedWork, hookFlags) {
  commitHookEffectListUnmount(hookFlags, finishedWork);
}

function commitHookEffectListUnmount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        const destroy = effect.destroy;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

export function commitPassiveMountEffects(root, finishedWork) {
  commitPassiveMountOnFiber(root, finishedWork);
}

function commitPassiveMountOnFiber(finishedRoot, finishedWork) {
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case HostRoot: {
      recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
      break;
    }
    case FunctionComponent: {
      recursivelyTraversePassiveMountEffects(finishedRoot, finishedWork);
      if (flags & Passive) {
        commitHookPassiveMountEffects(finishedWork, HookPassive | HookHasEffect);
      }
    }
  }
}

function recursivelyTraversePassiveMountEffects(root, parentFiber) {
  if (parentFiber.subtreeFlags & Passive) {
    let child = parentFiber.child;
    while (child !== null) {
      commitPassiveMountOnFiber(root, child);
      child = child.sibling;
    }
  }
}

function commitHookPassiveMountEffects(finishedWork, hookFlags) {
  commitHookEffectListMount(hookFlags, finishedWork);
}

function commitHookEffectListMount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

这里的逻辑就是递归所有节点,并判断是否存在副作用,如果存在则执行,和提交阶段对 DOM 的处理非常相似。

function updateEffect(create, deps) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps != null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

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

在 update 阶段,逻辑其实比较简单,就是根据 deps 是否变化,来判断是否设置 flags,设置了 flags 则会在 commit 阶段进入执行副作用的逻辑。但是对于 effect 对象来说,不管 deps 是不是有变化,都需要更新。