useEffect
useEffect 干嘛的
函数组件是纯函数,传入 props,返回对应的结果,再次调用,传入 props,依然返回同样的结果。
但现在有了 effect 之后,每次执行函数,额外执行了一些逻辑。例如网络请求
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [data, setData] = useState<number>(0)
function queryData(): Promise<number> {
return new Promise(resolve => {
let timer: number | null = setTimeout(() => {
resolve(200)
if (timer) {
clearTimeout(timer)
timer = null
}
}, 1000)
})
}
useEffect(() => {
queryData().then(r => {
setData(r)
})
}, [])
return (
<>
{data}
</>
)
}
export default App
注意,想用 async await 语法需要单独写一个函数,因为 useEffect 参数的那个函数不支持 async。
type EffectCallback = () => void | Destructor;
上面的代码就是执行函数组件的时候,额外去请求数据!然后set到data中去渲染
useEffect的第二个参数叫做依赖数组
react 是根据它有没有变来决定是否执行 effect 函数的,如果没传则每次都执行。
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [data, setData] = useState<number>(0)
function addData() {
setData(data + 1)
}
useEffect(() => {
console.log("effect!!!")
})
return (
<>
<button onClick={addData}>addData</button>
{data}
</>
)
}
export default App
也可以写任意的常量,因为它们都不变,所以不会触发 effect 的重新执行:
useEffect(() => {
queryData().then(r => {
setData(r)
})
}, [1, 2, 3])
但如果其中有个变化的值,那就会触发重新执行了
useEffect(() => {
console.log("effect!!!")
}, [1, 2, 3, new Date()])
这个数组我们一般写依赖的 state,这样在 state 变了之后就会触发重新执行了。
清除副作用(Clean - up)函数
当组件挂载、更新或卸载时,可能会有一些操作需要被执行,如订阅事件、设置定时器等,这些都是副作用。
当组件卸载或者重新执行useEffect
(由于依赖项变化)时(执行),就需要清除之前的副作用。
以定时器为例,假设在组件挂载时设置了一个定时器:
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [data, setData] = useState<number>(0)
function addData() {
setData(data + 1)
}
useEffect(() => {
let timer = setInterval(() => {
console.log(timer + ':', data * 2)
}, 1000)
}, [data])
return (
<>
<button onClick={addData}>addData</button>
<p>{data}</p>
</>
)
}
export default App
data 变化的时候,每一秒打印一次当前data*2后的值
点击多次后,会发现很多个定时器在执行。那么当effect重新执行的时候,就要清除本次的内部定时器 (这个return clean up 应该是 effect要重新执行的时候才会调用)
useEffect(() => {
let timer = setInterval(() => {
console.log(timer + ':', data * 2)
}, 1000)
return () => {
clearInterval(timer)
}
}, [data])
当依赖数组的data变化的时候会重新执行effect,当effect重新执行的时候,会执行当前effect的clean up 函数,清楚当前的定时器!
useLayoutEffect
和 useEffect 类似的还有一个 useLayoutEffect。那么这两者有什么区别呢!
1. 执行时机
useEffect:它是异步执行的。在浏览器完成渲染之后才会执行useEffect
中的代码。具体来说,当 React 更新 DOM 后,会等待浏览器下一次重绘(repaint)完成后,才会去执行useEffect
。这意味着如果useEffect
中有修改 DOM 的操作,会触发额外的重绘。
useLayoutEffect是同步执行,会阻塞浏览器的渲染,直到useLayoutEffect
中的操作完成。所以如果在useLayoutEffect
中有复杂或者耗时的操作,会导致页面渲染延迟,用户可能会感觉到页面加载变慢。
import React, { useState, useLayoutEffect, useEffect } from 'react';
const App = () => {
const [isOpen, setIsOpen] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const buttonRef = React.createRef();
const handleClick = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
if (isOpen && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const top = buttonRect.bottom;
const left = buttonRect.left;
setMenuPosition({ top, left });
}
}, [isOpen]);
return (
<div>
<button ref={buttonRef} onClick={handleClick}>打开菜单</button>
{isOpen && (
<div className="menu" style={{ position: 'absolute', top: menuPosition.top + 'px', left: menuPosition.left + 'px' }}>
<p>菜单选项1</p>
<p>菜单选项2</p>
<p>菜单选项3</p>
</div>
)}
</div>
);
};
export default App;
上面是useEffect的效果,dom渲染和useEffect的执行是异步执行的,上面闪动的情况是因为dom先渲染完成了。
menu 的 top和left都是0,所以一开始出现在最上面,然后effect执行完成后,重新set Top Left 重新渲染了新的位置。所以就出现了闪动的情况。
如果使用useLayoutEffect就可以解决闪动的问题,因为会阻塞dom的渲染优先计算出top left 直接渲染正确的位置。
2. 性能影响
useEffect,因为是异步执行,所以通常不会阻塞浏览器的渲染。对于那些不影响用户看到的初始渲染效果的副作用操作,如数据获取、订阅事件(这些操作不涉及布局相关内容),使用useEffect
是比较合适的,它可以让浏览器先把基本的页面内容渲染出来,再去处理这些副作用,有利于提高初始渲染的性能。
不过,如果在useEffect
中有大量的操作或者频繁触发useEffect
(由于依赖项频繁变化),可能会导致频繁的重绘,这在一定程度上会影响性能。
useLayoutEffect,由于是同步执行,会阻塞浏览器的渲染,直到useLayoutEffect
中的操作完成。所以如果在useLayoutEffect
中有复杂或者耗时的操作,会导致页面渲染延迟,用户可能会感觉到页面加载变慢。
但是,对于一些对布局精准性要求很高的操作,如计算一个复杂的布局元素的位置或者尺寸,并且这个结果需要在渲染时就准确呈现给用户,useLayoutEffect
就很合适,因为它能保证布局的准确性。
评论区