useEffect是什么
- useEffect 是一个专门react hook的一部分内容主要是为函数组件服务。
- 一般情况下我们可以看作componentDidMount, componentDidUpdate,componentWillUnmount 三个生命周期的结合体。
- 会在每次 render 的时候必定执行一次。
- 如果返回了函数,那么在下一次 render 之前或组件 unmount 之前必定会运行一次返回函数的代码。
- 如果指定了依赖数组,且不为空,则当数组里的每个元素发生变化时,都会重新运行一次。
- 如果数组为空,则只在第一次 render 时执行一次,如果有返回值,则同 3
- 如果在 useEffect 中更新了 state,且没有指定依赖数组,或 state 存在于依赖数组中,就会造成死循环。
在每一次渲染时都会锁住自己的props和state
我们来看一个例子
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
然后我们按照一下方式去执行
- 点击增加counter到3
- 点击一下 “Show alert”
- 点击增加 counter到5并且在定时器回调触发前完成大家猜一下结果是什么
链接: 代码结果查看地址.
我们可以看到答案是三
为什么呢
我们发现count在每一次函数调用中都是一个常量值。值得强调的是 — 我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。
实际上每渲染一次,每一次渲染都有一个新版本的handleAlertClick,并且每一次都有自己的count
effects的每一次渲染
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
有一个问题effect 是如何取到自己的count的状态值的呢?
答案是:
并不是count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同。
React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它
// During first render
function Counter() {
// ...
useEffect(
// Effect function from first render
() => {
document.title = `You clicked ${0} times`;
}
);
// ...
}
// After a click, our function is called again
function Counter() {
// ...
useEffect(
// Effect function from second render
() => {
document.title = `You clicked ${1} times`;
}
);
// ...
}
// After another click, our function is called again
function Counter() {
// ...
useEffect(
// Effect function from third render
() => {
document.title = `You clicked ${2} times`;
}
);
// ..
}
看起来是不是很高大上,其实原理很简单,大家当作js中的顺序执行就可以理解了
最后对于这个,我们思考一下下面代码的结果会是什么样子呢?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
结果如下所示:
你答对了没有
如果看懂的话,你可能会说,这不就是闭包嘛,对的,就是闭包
在组件内什么时候去读取props或者state是无关紧要的。因为它们不会改变。在单次渲染的范围内,props和state始终保持不变。(解构赋值的props使得这一点更明显。)
但是class 确不是这样的哦
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
class组件中的结果
使用useEffect 模仿class版本
在这里我们会用到useRef
关于useRef与ref 有什么关系我会后续在其他文章中解释。
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
防止effect
上文中我们知道,effect 每次运行都是独立的顺序调用。
那我们思考一下,如果这样是否会出现内存泄漏的问题呢,答案是肯定的,
effect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除:
React 渲染{id: 20}的UI。
浏览器绘制。我们在屏幕上看到{id: 20}的UI。
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。
去掉不必要的Effect的渲染
举个例子:
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={() => setCounter(counter + 1)}>
Increment
</button>
</h1>
);
}
但是我们的effect并没有使用counter这个状态。我们的effect只会同步name属性给document.title,但name并没有变。在每一次counter改变后重新给document.title赋值并不是理想的做法。
react 并不能区分effect的不同
这是为什么你如果想要避免effects不必要的重复调用,你可以提供给useEffect一个依赖数组参数(deps):
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]); // Our deps
```只有依赖数组改变时useEffect 才会改变
## 选择对的依赖项
我们看下面的代码
```function App() {
const [count, setCount] = useState(0)
useEffect(() => {
// 让resize事件触发handleResize
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const handleResize = () => {
// 把count输出
console.log(`count is ${count}`)
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+</button>
<h1>{count}</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
这段代码会画出我们熟悉的Counter例子。
现在我们如果点击那个+按钮,下面的数字0当然会增长,比如我们现在让count增长为1,这时候你去改变浏览器窗口的大小,console上会输出什么呢?
你可能预期这样输出:
count is 1
事实上,输出是这样:
count is 0
在第一次渲染中,count是0。因此,setCount(count + 1)在第一次渲染中等价于setCount(0 + 1)。既然我们设置了[]依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1)
我们可以依赖一下规则做规定
链接: (https://www.csdn.net/).
Effect自给自足
为了实现这个目的,我们需要问自己一个问题:我们为什么要用count?可以看到我们只在setCount调用中用到了count。在这个场景中,我们其实并不需要在effect中使用count。当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
我们来修改上面的例子让它包含两个状态:count 和 step。我们的定时器会每次在count上增加一个step值:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
我们用一个dispatch依赖去替换effect的step依赖:
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
函数也可以为依赖
如果某些函数仅在effect中调用,你可以把它们的定义移到effect中:
function SearchResults() {
// ...
useEffect(() => {
// We moved these functions inside!
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, []);
// ...
}
但是函数每次渲染都会改变这个事实本身就是个问题,比如有两个effects会调用getFetchUrl
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // 🔴 Missing dep: getFetchUrl
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // 🔴 Missing dep: getFetchUrl
// ...
}
但是如果两个依赖都会调用getFetchUrl,而它每次的渲染都不同,所以我们的依赖数组就会变得无用
function SearchResults() {
// 🔴 Re-triggers all effects on every render
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // 🚧 Deps are correct but they change too often
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // 🚧 Deps are correct but they change too often
// ...
}
解决办法
如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在effects中使用:
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, []); // ✅ Deps are OK
// ...
}
或者写成useCallback Hook:
// ✅ Preserves identity when its own deps are the same
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []); // ✅ Callback deps are OK
useEffect(() => {
const url = getFetchUrl('react');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK
// ...
}
useMemo 可以让我们对复杂的对象做类似的事情
function ColorPicker() {
// Doesn't break Child's shallow equality prop check
// unless the color actually changes.
const [color, setColor] = useState('pink');
const style = useMemo(() => ({ color }), [color]);
return <Child style={style} />;
}