Appearance
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 更新之后, 可能在渲染到浏览器之前
