Skip to content

useInsertionEffect

useInsertionEffect是为CSS-in-JS库提供的一个 hook,它让后者可以更合理地注入样式。

我们先看看前两个 effect 的执行实际

  • useLayoutEffect : 是 在 DOM 更新之后,浏览器绘制之前执行的
  • useEffect: 是在DOM 更新完成,且浏览器视图绘制完毕之后才执行

所以如果我们需要给一个组件元素单独添加一个样式的时候,最好的时候就是useLayoutEffect,但是这时候 DOM 属性已经更新完成了,我们再去赋值新的样式,可能导致 dom 元素的重新绘制,浪费性能,所以为了解决这个问题,在 DOM 更新之前提供了一个新的 Hook,即 useInsertionEffect,从而先去添加新的样式,然后再去更新 DOM 的属性,避免两次绘制。但是肯定能确保在浏览器渲染之前执行么,这是不确定的。

其执行时机应该是 肯定在 useLayoutEffect 之前,DOM 更新之后, 可能在渲染到浏览器之前

例子

jsx
function InsertionEffectComponent() {
  useInsertionEffect(() => {
    const style = document.createElement("style");
    style.innerHTML = `
         .css-in-js{
           position: absolute;
         }
       `;
    console.log("style: ", style);
    document.head.appendChild(style);
  }, []);

  return <div className="css-in-js"> useInsertionEffect </div>;
}

源码分析

js
/**
 * useInsertionEffect 初次渲染阶段执行函数
 *  Hook类型不同 为HookInsertion 所以其执行时机是在 commitMutationEfffect之前
 * @param {*} create
 * @param {*} deps
 * @returns
 */
function mountInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

function updateInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

从源码看,对于 useInsertionEffect与 useEffect 和 useLayoutEffect 的区别就是 Hook 副作用类型不同,其为 HookInsertion类型的,所以其执行时机不同

执行时机

DOM 更新之后,可能浏览器绘制之前

js
function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes
) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // 执行dom节点的插入操作 和 清除Placement标记
      commitReconciliationEffects(finishedWork);

      // 更新类型 副作用
      if (flags & Update) {
        try {
          // 执行元素的 useInsertionEffect destroy 回调函数
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return
          );
          // 执行元素的 useInsertionEffect create 回调函数
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
      }
      return;
    }
  }
}

从上述源码可以看出, 在 commit 第二个阶段按照深度优先遍历的规则从子节点到父节点的顺序执行 对应的 子 useInsertionEffect destroy 回调函数 => 子 useInsertionEffect create 回调函数 => 父 useInsertionEffect destroy 回调函数 => 父 useInsertionEffect create 回调函数

注意

但是这边不能确保 useInsertionEffect的执行时机是在 DOM 渲染之前。 即 React 官方文档原文说:useInsertionEffect may run either before or after the DOM has been updated. 因为执行 useInsertionEffect 之前已经执行了 commitReconciliationEffects(finishedWork) 只是如果在更新阶段的时候

  • 如果父节点正好是一个新的 DOM 节点,那么插入到父节点中的时候,这时候父节点在内存中,所以不会渲染到 浏览器中
  • 如果父节点已经在浏览器中,那么这时候执行插入到父节点中的时候,其实已经渲染到浏览器中了, 所以这在渲染到浏览器之后

所以useInsertionEffect的执行时机应该是 肯定在 useLayoutEffect 之前,DOM 更新之后, 可能在渲染到浏览器之前