useEffect

150 阅读7分钟

useEffect

  • hooks,可以在函数组件中模拟出类组件的生命周期
  • 如下:
    • componentDidMount 组件挂载完成
    • componentDidUpdate 更新完成
    • componentWillUnmount 组件卸载
  • 如何使用
    • 接收两个值:create(函数)、deps(数组或undefined)
    • create返回的值为destroy,也是一个函数,会在销毁周期中执行
    • 当deps中的值发生改变,执行create函数
const [num,setNum] = useState(0);

useEffect(()=>{
    console.log('执行1')
    return ()=>{
        console.log('销毁1')
    }
},[num])
useEffect(()=>{
    console.log('执行2')
    return ()=>{
        console.log('销毁2')
    }
},[num])

源码

  • hooks是函数组件独有功能,函数组件在初始化构建fiber树的时候会走renderWithHooks函数
  • renderWithHooks中将react的hook赋值,具体规则与useReducer一致
  • 相对于useReducer增加了一个新值,会在fiber的updateQueue种存储effect链表
    • 因为fiber结构是会被复用的,而函数通常会多次执行,那么每次执行的时候都需要将updateQueue清空,防止一直堆积
// renderWithHooks
/**
 * 渲染函数组件
 * @param {*} current 老fiber
 * @param {*} workInProgress 新fiber
 * @param {*} Component 组件定义
 * @param {*} props 组件属性
 * @returns 虚拟DOM或者说React元素
 */
export function renderWithHooks(current, workInProgress, Component, props) {
  currentlyRenderingFiber = workInProgress;//Function组件对应的fiber
  // effect用到updateQueue,每次进来先清除队列   
  workInProgress.updateQueue = null;
  //如果有老的fiber,并且有老的hook链表
  if (current !== null && current.memoizedState !== null) {
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }
  //需要要函数组件执行前给ReactCurrentDispatcher.current赋值
  const children = Component(props);
  currentlyRenderingFiber = null;
  workInProgressHook = null;
  currentHook = null;
  return children;
}

首次挂载

  • useEffect同样分为初次挂载和更新
  • 同样对应两套函数
  • react中只是在函数执行的时候透传参数,所以不罗列代码了

初始化effect实例

const HooksDispatcherOnMount = {
  useReducer: mountReducer,
  ...
  useEffect: mountEffect,
};

function mountEffect(create, deps) {
  return mountEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  // 初始化一个hooks实例,并添加到hooks链表中,与state和reducer一样
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  //给当前的函数组件fiber添加flags
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, undefined, nextDeps);
}
  • 先通过mountWorkInProgressHook初始化一个hooks实例,并将其关联到函数组件fiber的hooks链表上
  • 判断deps是否存在,undefined就是没有
  • 给fiber累加副作用PassiveEffect为1024,HookPassive为8
  • 然后将这个hook实例的memoizedState指向pushEffect的结果
// 初始化effect队列
function createFunctionComponentUpdateQueue() {
  return {
    lastEffect: null
  }
}
/**
 * 添加effect链表
 * @param {*} tag effect的标签
 * @param {*} create 创建方法
 * @param {*} destroy 销毁方法
 * @param {*} deps 依赖数组
 */
function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag, // 1024 | 8 = 1032
    create, // 传入的函数
    destroy, // 销毁函数
    deps, // 依赖项
    next: null
  }
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = 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;
}
  • pushEffect做了两件事:
    • 初始effect实例
    • effect实例添加到updateQueue上,同样是单向循环链表,lastEffect始终指向最后一个effect实例
  • 然后返回effect实例
  • 首次的effect并没有收集到destroy,因为destroy是create的执行结果,目前我们还并没有执行它,只是将其转换成effect实例

执行effect函数

  • 开头有讲到,effect的执行时机是在组件挂载完成组件销毁
  • 那么它的执行逻辑就在dom的挂载逻辑中,也就是CommitRoot函数里
// scheduleCallback = requestIdleCallback
function commitRoot(root) {
  //先获取新的构建好的fiber树的根fiber tag=3
  const { finishedWork } = root;
  if ((finishedWork.subtreeFlags & Passive) !== NoFlags
    || (finishedWork.flags & Passive) !== NoFlags) {
    if (!rootDoesHavePassiveEffect) {
      // 
      rootDoesHavePassiveEffect = true;
      scheduleCallback(flushPassiveEffect);
    }
  }
  console.log('开始commit~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
  //判断子树有没有副作用
  const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
  //如果自己的副作用或者子节点有副作用就进行提交DOM操作
  if (subtreeHasEffects || rootHasEffect) {
    //挂载dom
    commitMutationEffectsOnFiber(finishedWork, root);
    if (rootDoesHavePassiveEffect) {
      rootDoesHavePassiveEffect = false;
      rootWithPendingPassiveEffects = root;
    }
  }
  //等DOM变更后,就可以把让root的current指向新的fiber树
  root.current = finishedWork;
}

function flushPassiveEffect() {
  console.log('下一个宏任务中flushPassiveEffect~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    //执行卸载副作用,destroy
    commitPassiveUnmountEffects(root.current);
    //执行挂载副作用 create
    commitPassiveMountEffects(root, root.current);
  }
}
  • finishedWork就是我们本次要渲染的fiber树
  • fiber的副作用会向上冒,那么它的父级可以通过subtreeFlags统计副作用
  • 那么只需要判断顶层的副作用是否有1024就可以得到后代节点中是否有effect要执行
  • rootDoesHavePassiveEffect为全局变量,默认为false,我们把它当做一个开关
  • 如果有effect要执行,打开开关
    • 然后注册一个异步事件,在异步事件中去执行effect链表的执行
  • 然后渲染完成dom后关闭开关,并且给全局变量rootWithPendingPassiveEffects赋值为根实例
  • 注册的effect因为是异步事件,所以它的执行时机晚于dom渲染

开始执行

  • 走的是flushPassiveEffect函数
  • 假设effect是存在返回函数的,那么它的执行顺序应该为先执行所有destroy函数再执行所有create函数
  • 那么逻辑就拆分成两部分
    • 执行destroy的函数commitPassiveUnmountEffects
    • 执行create的函数commitPassiveMountEffects

commitPassiveUnmountEffects

  • 通过递归的形式依次遍历fiber,然后找到函数fiber上的updateQueue,执行effect实例上的destroy
  • 有一个误区,它并不是找到所有destroy然后一次性执行,而是找到一个函数fiber就执行它队列中的destory
  • 查找顺序:儿子->儿子的sibling->父
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) {//1024
        commitHookPassiveUnmountEffects(finishedWork, HookHasEffect | HookPassive);
      }
      break;
    }
  }
}
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) {
    //获取 第一个effect
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //如果此 effect类型和传入的相同,都是 9 HookHasEffect | PassiveEffect
      if ((effect.tag & flags) === flags) {
        const destroy = effect.destroy;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect)
  }
}
  • demo
const A = () => {
  useEffect(() => {
    console.log("A");
    return () => {
      console.log("销毁A");
    };
  });
  return <div>A</div>;
};
const B = () => {
  useEffect(() => {
    console.log("B");
    return () => {
      console.log("销毁B");
    };
  });
  return <div>B</div>;
};
const App = () => {
  const [num, setNum] = useState(0);
  useEffect(() => {
    console.log("App");
    return () => {
      console.log("销毁App");
    };
  });
  return (
    <div>
      <button
        onClick={() => {
          setNum(num + 1);
        }}
      >
        按钮
      </button>
      <A />
      <B />
    </div>
  );
};
  • 执行顺序:
    • 首次执行:A->B->APP
    • 点击按钮,触发视图更新:销毁A->销毁B->销毁APP->A->B->APP

commitPassiveMountEffects

  • 执行create,与销毁执行顺序和逻辑几乎一致
  • 唯一的区别在于会将create函数的执行结果放到effect实例的destory
  • 还有一个点:
    • 首次是不比对deps的,一定会执行create
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) {//1024
        commitHookPassiveMountEffects(finishedWork, HookHasEffect | HookPassive);
      }
      break;
    }
  }
}
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) {
    //获取 第一个effect
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      //如果此 effect类型和传入的相同,都是 9 HookHasEffect | PassiveEffect
      if ((effect.tag & flags) === flags) {
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect)
  }
}

更新

  • 触发重新渲染后会重新计算fiber树,那么还是要走renderWithHooks
  • 给react的钩子引用先换一套更新逻辑
  • 清除updateQueue
const HooksDispatcherOnUpdate = {
  useReducer: updateReducer,
  ...
  useEffect: updateEffect,
}
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;
  //上一个老hook
  if (currentHook !== null) {
    //获取此useEffect这个Hook上老的effect对象 create deps destroy
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 用新数组和老数组进行对比,如果一样的话
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        //不管要不要重新执行,都需要把新的effect组成完整的循环链表放到fiber.updateQueue中
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  //如果要执行的话需要修改fiber的flags
  currentlyRenderingFiber.flags |= fiberFlags;
  //如果要执行的话 添加HookHasEffect flag
  //Passive还需HookHasEffect,因为不是每个Passive都会执行的
  hook.memoizedState = pushEffect(HookHasEffect | hookFlags, create, destroy, nextDeps);
}
// deps比对函数,有一个不一样就为false
function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null)
    return null;
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}
  • 还是先复用老hook实例
  • 执行步骤与初次并无太大区别
    • 唯一的区别在于会进行新旧deps比较,
    • 如果相同,就将本次的effect的tag设置为8初始化的时候我们设置的是9
  • 那么走上边的CommitRoot后,走effect事件执行,
  • 在遍历updateQueue的时候,我们传入的校验比对也是9
    • 如果9===9,这跟初次一样,执行函数
    • 那么如果deps相同,我们这次就将effect的tag设置为8了,那么显然是不同的,所以也就不会执行create和destroy了