useLayoutEffect与useEffect名字很像,用法也大致类似,但两者执行时机不同:
useEffect 的函数会在组件渲染到屏幕之后执行
useLayoutEffect则是在DOM结构更新后、渲染前执行,相当于有一个防抖效果
根据上述特性,可以预计到如果在useEffect中瞬时连续更新某个状态,那么页面会发生多次渲染,现在举例测试。
import { useState, useEffect, useLayoutEffect } from "react";
import * as ReactDOM from "react-dom";
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log("render", value);
return (
<div onClick={() => setValue(0)}>value: {value}</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
这个测试的意思是,在点击div的时候将value设为0,但在useEffect中又将其设为一个随机值。这样相当于value这个状态快速连续的变更了两次。其表现结果为
可以看到这里打印出了两个结果,也就是value连续变更了两次,在实际观察页面的时候,这个div是有闪动的,这在交互上和性能上是绝对需要避免的。这个例子中组件的变化顺序是:
- click setState (value)
- 虚拟 DOM 设置到真实 DOM 上
- 渲染
- 执行useEffect回调
- setState(value)
- 虚拟 DOM 设置到真实 DOM 上
- 渲染
一共执行了两次渲染:3和7
再看useLayoutEffect的表现:
import { useState, useEffect, useLayoutEffect } from "react";
import * as ReactDOM from "react-dom";
function App() {
const [value, setValue] = useState(0);
useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log("render", value);
return (
<div onClick={() => setValue(0)}>value: {value}</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
这里的打印结果和上面一样,但页面表现却不一样,页面并没有出现闪动,而是正常渲染。
这个例子中组件的变化顺序是:
- click setState (value)
- 虚拟 DOM 设置到真实 DOM 上
- 执行useLayEffect回调
- setState (value)
- 虚拟 DOM 设置到真实 DOM 上
- 渲染
只会进行一次页面渲染
以上,照此看useLayoutEffect比useEffect更好,那么是否可以全部使用前者来代替后者呢?
但官网提到的一些应该注意的点:
- 应先使用useEffect,若出问题再尝试useLayoutEffect;
- useLayoutEffect与componentDidMount,componentDidUpdate的调用阶段是一样的;
- 针对服务端渲染:useEffect和useLayoutEffect都无法在JS代码加载完成之前执行。(笔者暂未接触服务端渲染,不清楚这意味着什么)
详细的关于useEffect和useLayoutEffect执行时机见这篇:深入理解 React useLayoutEffect 和 useEffect 的执行时机