问题一:闭包
前提声明,下面写的代码会带来相当的心智负担,一般写代码的时候不应这样写,这么做只是用于讲解useEffect的闭包。
假设有这样一个需求,我们要在每个页面中自定义document.title,当页面退回到原先的页面的时候,自动把原先页面的title赋值给document.title。该如何实现呢,下面是用useEffect的实现示例:
const Test = (title: string) => {
const oldTitle = document.title
useEffect(() => {
document.title = title
}, [title])
// 当页面注销的时候恢复为oldTitle
useEffect(() => {
return () => {
document.title = oldTitle
}
}, [])
}
之所以当页面注销的时候可以恢复为旧的oldTitle,主要就是利用了闭包的能力,将最早的oldTitle结果保存住了,没有因为函数执行结束而释放内存。如果理解不清晰,再看下面一个例子:
const testClosure = () => {
let num = 0;
const effect = () => {
num += 1;
const message = `num value in message:${num}`;
return function unmount() {
console.log(message);
};
};
return effect;
}
接下来的代码执行顺序如何?
const add = testClosure()
const unmount = add()
add()
add()
unmount()
如果你觉得add函数执行了3次,那么num累计加到3就错了,实际上因为闭包的原因,const unmount = add()之后,unmount里面的结果就不再发生变化了。所以打印出来的就是1,这也是上面useEffect因为闭包导致可以赋值成功的原因。
但是如果我们写代码的时候因为追求闭包,而去故意设计useEffect会带来不小的心智负担。就像上面代码中,第二个useEffect,title发生了变化,但是useEffect却不对此进行监听,不是很符合响应式的理念。
更好的方式是使用useRef来实现。useRef具有全局不变性,相当于一个存储,可以把oldTitle储存起来了,实现同样的效果,就不用去故意设计和考虑useEffect的闭包了。
const Test = (title: string) => {
const oldTitle = useRef(document.title).current
useEffect(() => {
document.title = title
}, [title])
// 当页面注销的时候恢复为oldTitle
useEffect(() => {
return () => {
document.title = oldTitle
}
}, [oldTitle])
}
问题二:useEffect依赖更新无限循环
我们知道,useEffect会监听方括号中的依赖,当依赖发生变化的时候,就会触发useEffect的更新。举一个例子:
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
// 当obj是基本类型的时候,就不会无限循环
// 当 obj是对象的时候,就会无限循环
// 当 obj 是对象的state时,不会无限循环
// const [obj, setObj] = useState({ name: "Jack" });
// const obj = 1;
const obj = {name: 'Jack'}
const [num, setNum] = useState(0);
useEffect(() => {
console.log("effect");
setNum(num + 1);
}, [obj]);
return (
<div className="App">
{num}
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
执行这个例子会触发无限循环,num值不停加1。背后是因为obj的结果不一样。
首先在第一次加载的时候上述代码会执行一遍。之后setNum会导致num+1,上述代码会再次重新执行,生成新的obj和原先的obj不是一个obj,触发useEffect再次执行,又会再次导致setNum执行,于是整个程序就一直不停的执行了。如果obj是基本类型的话,因为两次obj结果是一样的,所以不会触发useEffect再次执行。
上面的问题还是和闭包有关,也可以用useRef来解决,或者把对象使用useState处理。
总结来说 基本类型可以放到依赖里;组件状态,可以放到依赖里;非组件状态的对象,绝不可以放到依赖里
const obj = {name: 'Jack'}
const obj2 = {name: 'Jack'}
// false
obj === obj2
const val1 = 1
const val2 = 1
// true
val1 === val2
运行尝试链接