官方解释
官方解释,这两个hook基本相同,调用时机不同,请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect。还举了个例子,譬如你想测量DOM元素时候,使用useLayoutEffect。个人感觉举例不恰当,测试DOM我也完全可以在useEffect中测量啊。说如果需要在paint前改变DOM,更合适。
我做过测试,譬如一个div尺寸是200 * 200,我想改成100 * 100,如果写在useEffect中,确实会造成页面抖动,写在useLayoutEffect中可以避免。
redux-react-hook 中的妙用
redux-react-hook库中有段代码使用了useLayoutEffect,用来避免组件render两次。
这里的useIsomorphicLayoutEffect就是useLayoutEffect(因为库要区分是浏览器还是SSR,所以上面做了处理)
// We use useLayoutEffect to render once if we have multiple useMappedState.
// We need to update lastStateRef synchronously after rendering component,
// With useEffect we would have:
// 1) dispatch action
// 2) call subscription cb in useMappedState1, call forceUpdate
// 3) rerender component
// 4) call useMappedState1 and useMappedState2 code
// 5) calc new derivedState in useMappedState2, schedule updating lastStateRef, return new state, render component
// 6) call subscription cb in useMappedState2, check if lastStateRef !== newDerivedState, call forceUpdate, rerender.
// 7) update lastStateRef - it's too late, we already made one unnecessary render
useIsomorphicLayoutEffect(() => {
lastStateRef.current = derivedState;
memoizedMapStateRef.current = memoizedMapState;
});
看得很懵逼,讲了如果用useEffect会带来什么问题,我模拟了很久终于模拟出来作者描述的问题(意图好猜,模拟时候有个细节很难处理)
模拟场景简化
我有一个数据store(对redux的store),一个组件App,组件中使用了useA和useB两个自定义hook(这对应两次调用redux-react-hook的useMappedState)。
当我一个操作,改变store时候,去调用订阅者即A和B,A和B改变会触发App重新render。这里有个问题,A和B都是订阅者,会触发两次App重新render,作者想避免,所以会在use的时候做下处理,使用useEffect的话,会出现bug,无法如愿,下面就来模拟这个过程。
代码实现
function App() {
console.log('%c App render--start-->', 'color:blue')
const a = useA();
const b = useB();
function doSomething() {
// dispatch();
setTimeout(dispatch, 0)
}
console.log('%c App render--end-->', 'color:red')
return (
<div>
<p>a: {a}</p>
<p>b: {b}</p>
<p><button onClick={doSomething}>dispatch</button></p>
</div>
)
}
function useA() {
console.log('---a--hook-->')
const [trigger, setTrigger] = useState(0);
useEffect(() => {
console.log('--useA--useEffect-->')
memoStore = store;
});
useEffect(() => {
const fn = subsriber(() => {
console.log('--useA--注册函数--->', memoStore, store);
if(store !== memoStore) {
setTrigger(Math.random())
}
});
return () => unSubsriber(fn);
}, []);
return store;
}
function useB() {
console.log('---b--hook-->')
const [trigger, setTrigger] = useState(0);
useEffect(() => {
console.log('--useA--useEffect-->')
memoStore = store
});
useEffect(() => {
const fn = subsriber(() => {
console.log('--useB--注册函数--->', memoStore, store);
if(store !== memoStore) {
setTrigger(Math.random())
}
});
return () => unSubsriber(fn);
}, []);
return store;
}
简化的redux:
let store = 6;
let memoStore = 6;
const newStore = 8;
const subsriberList = new Set();
function subsriber(fn) {
subsriberList.add(fn);
return fn;
}
function unSubsriber(fn) {
subsriberList.delete(fn)
}
function dispatch() {
memoStore = store;
store = newStore;
subsriberList.forEach(fn => fn())
}
这里有一个非常重要的关键点,就是App组件中的doSometing中,dispatch一定要写在setTimeout中,否则react自动帮你优化了,模拟不出来想要的场景。
分析
点击按钮时候,改变了store: 6 -> 8,触发了订阅者自定义hook A和B的订阅事件。按理会触发两次App render,但是我们做了优化,在useA和useB的时候,会用新状态去覆盖旧状态,然后在订阅事件中,会对比新老状态,一致的话,就不去触发自定义hook改变了,也就不会触发App render了。
但是使用effect的话,实际执行过程是这样的:
可以看到,App依旧render了两次,其中主要问题就出在useEffect注册的函数在什么时候执行,从流程图中可以看到,其不是在App组件树 render结束后立即执行的(我也不知道什么时候执行,还请哪位大佬指点),js会继续执行后面的代码(B的订阅),这个时候old=new还没有执行,所以依旧触发了第二次App组件render。
更改useEffect为useLayoutEffect
useA
...
useLayoutEffect(() => {
console.log('--useA--useLayoutEffect-->')
memoStore = store;
});
...
useB
...
useLayoutEffect(() => {
console.log('--useA--useLayoutEffect-->')
memoStore = store
});
...
可以看见关键点是,layoutEffect队列在组件树render结束后,会立刻同步执行(个人感觉是的),所以在第一次App render结束后,old和new就相同了,在执行B订阅时候,就会根据条件,不再触发App render了。
总结
// 一定要加setTimeout模拟异步操作,否则实验不出来上面的流程的
setTimeout(()=>{
renderApp1(); // 一些会条件性触发组件重新render的代码
exeLayoutEffectList(); // 组件树构建完毕,会同步执行useLayoutEffect中的代码
code1(); // 一些js代码
code2(); // 一些js代码
// 所有代码都执行完毕后,浏览器渲染结束后,会调用useEffect中的代码
// 或者接到下一次组件刷新(re-render)指令,会将上一次effect队列执行完毕。我根据试验猜的
exeEffectList();
renderApp2(); // 一些会条件性触发组件重新render的代码
}, 0)
主要就是effect和layoutEffect队列的执行阶段,layout会在组件树构建完毕或者刷新完毕后同步立刻执行。effect会等其他js代码执行完毕后执行(或者遇到下一次刷新任务前)
回过头再看react关于useLayoutEffect的官方文档:
The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
Prefer the standard useEffect when possible to avoid blocking visual updates.
- 和useEffect相同,是指他们都在组件树构建完毕之后执行的
- 但是useLayout是在DOM突变之后立即执行的,突变是指什么?是指类似组件构建完毕之后,appendChild(reactTree)这种操作吗?
- 可以肯定的是,是在组件树构建完毕后同步执行,之后才会去执行后面的js代码
- 使用他来读取DOM布局尺寸,我倒感觉应该是写成设定DOM布局尺寸,这样可以防抖动,同步读取DOM布局尺寸想不懂有什么用
- useLayoutEffect队列中的任务,会在浏览器paint之前执行(可以用来防抖)
- 尽可能使用useEffect来避免阻塞视觉更新(见上条,阻碍paint)
吐槽
英语太差,好多概念模模糊糊的,但是好像看过国外文章,也有吐槽react的几个概念含糊不清的。