学习了近半个多月的react,也看了网上许多文章,发现现阶段用的最多的两个hook就是useEffect和useState,但总感觉对useEffect的理解还是太浅,还是停留在把它当成之前class组件中的生命周期去看待。最近有幸看了篇关于useEffect文章,看完感觉大有收获,下面就把一些收获和总结分享给大家。
有一些话非常经典,简单明了的概括了一些具体的知识。在此记录下来
- 每一次渲染都有它自己的props和state
- 每一次渲染都有它自己的事件处理函数
- 每一次渲染都有它自己的useEffect
- 有时候你可能想在useEffect的回调函数里读取最新的值而不是捕获的值。最简单的实现方法是使用refs
- 组件内的每一个函数(包括事件处理函数,useEffect,定时器或者API调用等等)会捕获定义它们的那次渲染中的props和state。
- React统一描述了初始渲染和之后的更新
- React只会更新DOM真正发生改变的部分,而不是每次渲染都大动干戈。
- React并不能猜测到函数做了什么如果不先调用的话。
- 如果当前渲染中的这些依赖项和上一次运行这个useEffect的时候值一样,因为没有什么需要同步React会自动跳过这次useEffect
- 如果你设置了依赖项,useEffect中用到的所有组件内的值都要包含在依赖中。 这包括props,state,函数 — 组件内的任何东西。
- 当我们想要根据前一个状态更新状态的时候,我们可以使用
setState的函数形式: - 如果我们有两个互相依赖的状态,或者我们想基于一个prop来计算下一次的state,它并不能做到。这时候可以考虑useReducer。
- 当我们需要将函数传递下去并且函数会在子组件的effect中被调用的时候,
useCallback是很好的技巧且非常有用。
1. 先了解下函数式组件渲染的基本知识。
看下面一段简单的代码:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
add
</button>
</div>
);
}
这段代码很简单,就是实现点击按钮count累加的功能。但是以前使用vue的经验总告诉我,它的实现原理是这样的:当count改变时,只是这行代码(<p>You clicked {count} times</p>)中的count的值发生了变化,然后渲染在页面上,就这样不停的累加,组件内的其它内容是不会重新执行的。看完上面的文章才发现函数式组件的渲染根本不是这样的,点击按钮时,setCount()触发组件更新,函数重新执行,这时count的值是最新的值,比如说从0累加到1,<p>You clicked {count} times</p>,这行代码中的count只是去捕获最新的值: 1,并显示出来,仅此而已。
这只是简单的累加,如果我把它放在一个setTimeout中呢?
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)}>
add
</button>
<button onClick={handleAlertClick}>
show
</button>
</div>
);
}
这段代码的执行结果是:无论你点击add点多少次,当你点show的时候,只会拿到点show时,当前组件内的count。这也足以证明了每次组件更新,都会重新执行组件函数,执行时组件内的状态都是跟其它时候组件函数执行独立开的,是唯一的,所以你无论是在点show之前还是之后点击add,show弹出的alert只是那个时候组件内的值,不会变化。这也跟hooks全面拥抱函数式编程中的纯函数相呼应-只要给出相同的参数,就能得到相同的结果。由此也可以推理到,在函数式组件中的props、变量、state和useEffect都适用这个原理,只能拿到当前渲染的组件内的值,并且每次渲染都是一个新的函数和变量。
了解了函数式组件的渲染知识,再来看useEffect这个hook。
2. 再看useEffect的渲染
来看下官方文档的例子
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>
);
}
useEffect中是如何拿到最新的count的值呢? 其实和组件内的事件处理函数是一样的,只是去捕获当前组件内最新的值,没有什么其它的特殊处理。
并不是
count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同。
每一次count值,都来自那次特定的渲染。
TODO 其实我自己在这里有一个疑问,setCount()执行后,组件函数重新执行,count是怎么在函数中能保持累加的呢?可能有人说是用闭包,但是据我理解,闭包要返回一个函数,并在这个函数里使用了上层函数定义的变量,会形成闭包,这里的函数式组件,并没有返回一个函数啊,只是返回一个JSX,怎么就会记住上次的变量,并在下次执行的时候去累加的呢?先把这个问题放在这,以后慢慢会懂的。
3. Effect中的清理是如何运行的?
网上好多好文章,说到useEffect的清理的时候,只是简单的说等于unmount的钩子,其它的并没有多说。
本质上它的作用是消除副作用,比如取消订阅。其实还有一个作用,在清理的时候,会拿到上次函数组件中的值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
return () => {
console.log(count)
}
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
add
</button>
</div>
);
}
还是这个例子,我在里面加了一个清理函数,当前我点击add的时候,log中的count是0,还是1呢?答案是“0”,清理中的count并不是新最的,是更新前的值。
useEffect的清除并不会读取“最新”的props。它只能读取到定义它的那次渲染中的props值
TODO 这里我也有些疑惑,当点add时,最新的定义是累加后的值啊,为什么在清理中还能拿到上次定义的值呢??
4. 丢掉生命周期的概念
由于使用了vue很长时间,脑子里会形成生命周期这个思维定式,之前看了一些文章,也先入为主,总说effect就可以理解为class组件时的生命周期的概念,其实这样理解是不准确的。
React会根据我们当前的props和state同步到DOM。 “mount”和“update”之于渲染并没有什么区别。
我们应该用新的函数式思维来思考组件中的useEffect,useEffect能使你根据props和state,同步reactTree之外的东西。
如果函数每次执行,都执行一次useEffect,如果这里面有数据请求之类的处理,就不太高效。那我们该如何做呢?
5. 根据依赖,对比每次useEffect((),[dpes])
React处理DOM的更新的办法,是只更新变化的地方,同理,我们也能用处理DOM的办法,来处理useEffect。由于useEffect是个函数,组件每次更新,React并不能猜测到函数做了什么如果不先调用的话。所以需要一个依赖数组deps来判断当前useEffect是否有变化,来避免重复调用。
- 如果当前渲染中的依赖项和上一次useEffect中运行的一样,React会跳过这次useEffect。
- 如果当前渲染中的依赖项数组中有一个变化了,React也会执行这次useEffect
其实现在就能很好的理解当依赖数组为空时,为什么useEffect只在初始化时执行一次了。因为第二次执更新的时候,依赖项为空,组件会对比两个[],发现依赖项没变化,就不会执行useEffect了。 好了,到这里其实讲明白了useEffect渲染和执行过程。
然而useEffect的依赖项也有一些知识和技巧
- 在依赖中包含所有useEffect中用到的组件内的值
- 修改useEffect内部的代码以确保它包含的值只会在需要的时候发生变更
比如有下面代码,想想下面代码的执行结果是什么?
const [count,setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log('count', count)
setCount(count + 1); }, 1000);
return () => clearInterval(id);
}, []);
return (<div>{count}</div>)
现在我们能很好的理顺这个流程了,上面代码只在初始化时在页面上打印了1,log里的日志每秒都会输出0。
因为初始化的时候,useEffect执行了一次,输出了console.log('count', count)是0,触发setCount(count + 1),函数重新执行,这时count是1,触发页面更新,打印1,但当函数重新执行时,依赖项为[],不会触发useEffect,也就不会触发组件更新,页面不会更新。所以页面只输出1。
TODO 但这里有一点不太明白,为啥更新时,没执行useEffect,但console.log()里依然会会每1秒输出一次count呢?
我们要把useEffect中用到的变量都写在依赖中,就可以实现正确的功能
const [count,setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log('count', count)
setCount(count + 1); }, 1000);
return () => clearInterval(id);
}, [count]);
return (<div>{count}</div>)
有时候减少依赖项,也是一种优化的方法,比如上面的例子,在useEffec中,我们只在setCount中用到了count,我们可以用setCount的函数式形式,来实现相同的功能。
当我们想要根据前一个状态更新状态的时候,我们可以使用
setState的函数形式。你可以认为它是在给React“发送指令”告知如何更新状态。
const [count,setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log('count', count)
setCount(count => count + 1); }, 1000);
return () => clearInterval(id);
}, []);
return (<div>{count}</div>)
即使是setCount(c => c + 1)也并不完美。 它看起来有点怪,并且非常受限于它能做的事。举个例子,如果我们有两个互相依赖的状态,或者我们想基于一个prop来计算下一次的state,它并不能做到。这时候可以考虑useReducer。
useReducer我还在理解中,这里先不说啦。
6. 把函数移到Effects里
如果某些函数仅在useEffect中调用,你可以把它们的定义移到useEffect中
function SearchResults() {
// ...
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, []); // ✅ Deps are OK
// ...
}
这样做让我们可以不用再考虑依赖,并且我们的依赖也是正确的,我们并没有依赖组件的任何值。
如果我们现在要依赖query来查询的话,可以改成这样,只有query改变的情况下才会重新查询
function SearchResults() {
const [query, setQuery] = useState('react');
useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query; }
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, [query]); // ✅ Deps are OK
// ...
}
有时候,可能几个useEffect里都用到了getFetchUrl,我们会把getFetchUrl提到useEffect外边,封装成一个公共的函数
function SearchResults() {
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(() => {
const url = getFetchUrl('react');
}, [getFetchUrl]);
useEffect(() => {
const url = getFetchUrl('redux');
}, [getFetchUrl]);
}
但上面的写法console.log只会执行一次,依赖相当于没起作用。 解决办法有两个:
1. 如果一个函数没有使用组件内的任何值,你应该把它提到组件外面去定义,然后就可以自由地在useEffect中使用。他没有使用组件内的任何值,根本不用定义依赖。
function getFetchUrl(query) {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
useEffect(() => {
const url = getFetchUrl('react');
}, []); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
}, []); // ✅ Deps are OK
// ...
}
2. 或者可以用useCallback来包装一下
function SearchResults() {
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [])
useEffect(() => {
const url = getFetchUrl('react');
}, [getFetchUrl]); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl('redux');
}, [getFetchUrl]); // ✅ Deps are OK
// ...
}
如果现在要把这个逻辑升级一下,用一个输入框动态输入query,我们可以这样实现
function SearchResults() {
const [query, setQuery] = useState('react')
const getFetchUrl = ((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query])
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]); // ✅ Deps are OK
useEffect(() => {
const url = getFetchUrl();
}, [getFetchUrl]); // ✅ Deps are OK
return (<div>
<input
type="text"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</div>)
}
对于通过属性从父组件传入的函数这个方法也适用:
function Parent() {
const [query, setQuery] = useState('react');
const fetchData = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
return <Child fetchData={fetchData} />
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]);
// ...
}
useCallback和useMemo使函数可以参与到数据流中,他们依赖项的值变化了,函数就才会重新执行
最后,还有如果处理请求数据的竞态问题
请求更早但返回更晚的情况会错误地覆盖状态值,这被叫做竞态。 解决方法可以用一个布尔值来来做判断
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {didCancel = true; };
}, [id]);
// ...
}
好了,以上就是读完《useEffect 完整指南》一些总结,有错误之处,还望大家指正。其中一些知识点自己也在理解之中。
文章中有些特别好的小例子,还是建议大家去看看原版的文章 useEffect 完整指南
同时也要感谢大神ssh的分享,无意间看了他的文章《写给初中级前端的高级进阶指南(万字路线)》,才知道这篇宝藏文章的。大家也可以去看一下,收获很多。