React:useEffect中的setInterval
React官网的FAQ有这么一个问题“如果我的 effect 的依赖频繁变化,我该怎么办?”
1、BUG描述:你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug
会产生BUG的代码
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
运行结果: count 永远不会超过 1
count的值为:0、1、1、1......
2、疑惑:一开始完全不理解count的值为什么最大为1,认为setCount每次都会使count加1且重新渲染新值,但事实与之相反,后来在官网的修复代码中看到了一个关键词“作用域”,猜测不理解的原因基本上是对“作用域”了解不深。
紧接着马上去啃完了《你不知道的JavaScript》中第一部分“作用域和闭包”的知识,然后画出BUG代码对应的作用域链气泡图后瞬间清晰了
3、分析解决
(1)、分析
BUG代码的作用域链气泡图
由上图可以看出,Counter组件在挂载时,会产生由三个作用域嵌套而成的作用域链,当定时器setInterval每一次执行时,都是从此作用域链(挂载时形成)寻找count值,因为挂载期间count的初始值始终为0,所以定时器每一次执行开始前count都为0,执行结束后count都为1。
尽管由于定时器的存在,组件始终会一直重新渲染,但定时器的回调函数是挂载期间定义的,所以它的闭包永远是对挂载时Counter作用域的引用,故count 永远不会超过 1。
(2)解决:方案一为最佳
方案一:使用setCount回调形式更新count
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
return <h1>{count}</h1>;
}
setCount
函数的身份是被确保稳定的,所以可以放心的省略掉
此时,setInterval
的回调依旧每秒调用一次,但每次 setCount
内部的回调取到的 count
是最新值(在回调中变量命名为 c
)。
方案二:给useEffect添加count依赖
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, [count]); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
作用域链气泡图
上图可以看出,由于给useEffect添加了第二个依赖参数count,每一次定时器的执行改变了count值后,组件都会重新渲染,且effect都会重新执行,所以新的作用域链会生成(见上图右侧),此后的每一次重新渲染,访问的都是新Counter作用域中的新count值,即:1、2、3...
方案三:使用useReducer把state更新逻辑移到effect 之外
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'add':
return { count: state.count + 1 } // *
default:
throw new Error()
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
clearInterval(id)
};
}, []);
return <h1>{state.count}</h1>;
}
将整个状态管理转移到reducer(见*行)将消除useEffect
回调中对本地状态的任何引用。
我们的useEffect
主体更加简单易读,并且我们不再需要担心陈旧状态被困在闭包中:我们所有的更新都是通过对单个 reducer 的调度发生的。使用 reducer 有助于将读取与写入分开。我们的useEffect
身体现在只关心调度动作,这会产生新的状态,在它关心读取现有状态和写入新状态之前。
方案四:useRef
万不得已的情况下,如果你想要类似 class 中的 this
的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内持续存在。
function Counter() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
useEffect(() => {
countRef.current += 1; // *
})
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current)
setCount(countRef.current)
}, 1000)
return () => clearInterval(id)
}, [])
return (
<div>
<div>count: {count}</div>
</div>
)
}
每一次重新渲染后,countRef.current
就会递增一次(见*行)。利用countRef.current
来保存并递增count
的值。每过一秒,定时器就会取出countRef.current
的值并重新渲染组件。
方案五:局部变量法
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
let counterMutable = count;
const id = setInterval(() => {
counterMutable++
setCount(counterMutable)
}, 1000)
return () => clearInterval(id)
}, [])
return (
<div>
<div>count: {count}</div>
</div>
)
}
作用域链气泡图
组件挂载时,在effect中保存外部作用域的count值,setInterval回调中的setCount在每一次执行前都会把counterMutable加1,并用counterMutable去更新count,这保证了count的值会一直正常递增。
setCount的作用之一是重新渲染组件,即重新执行函数组件,重新执行函数后的新count值都会在新counter作用域中(见上图右侧),不会改变原来counter作用域中count的值,但定时器却只会使用挂载时的作用域链中的值。
转载自:https://juejin.cn/post/7103789057670381598