2023年了~useEffect你真的会用嘛
前言
这篇文章是我依据a-complete-guide-to-useeffect本人总结进去的一些重点~
依照程序顺次如下:
- React单向数据流的渲染
- Effect的执行机会
- 不要对Effect扯谎:依赖数组要怎么设置
- 应用
setState
和useReducer
将上报行为和状态更新解耦 - 函数是否能够作为Effect的依赖呢?
同时咱们能够在浏览的时候时刻问本人以下问题:
- 如何用useEffect模仿componentDidMount生命周期?
- 如何正确地在useEffect里申请数据?[]又是什么?
- 我应该把函数当做effect的依赖吗?
- 为什么有时候会呈现有限反复申请的问题?
- 为什么有时候在effect里拿到的是旧的state或prop?
每一次渲染都有它本人的props和state
function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p>+ <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}
上述例子中,对于<p>You clicked {count} times</p>
中的count
,可能react的初学者会认为其是响应式的,也就是会主动监听并且更新渲染。
但实际上,它只是一个一般的变量count
,没有任何的data bounding
。咱们在每一次调用setCount
的时候,实际上是React应用新的count
值来重复渲染组件。第一次是0,第二次是1,以此类推。。。
除了变量,那么事件处理函数呢?
function Example() { 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> );}
咱们设想一下:
- 首先间断点击 Click me 两次
- 再点击一次 Show alert
- 持续点击 Click me
在点击alert的3秒过后,其显示的值会是什么呢?
答案是2!!,这也证实了咱们上述的论断。即事件处理函数跟变量一样,也是独立属于以后的渲染。
每次渲染都有它独立的Effect
咱们来看看官网上的例子:
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
的值发现了变动,而是Effect每一次渲染都不雷同,每一次Effect都捕捉了以后渲染的count
值」
<div style="color:#d2568c;border-left:5px solid;padding:6px"><span style="font-weight:bold;">「React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。」</span></div>
useEffect
每一次渲染都有它本人的......所有
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> );}
咱们来思考一下上述代码,咱们给每一次渲染都增加一个延时函数。当咱们疯狂点击Click me的时候,会产生什么呢?
答案是:「程序打印0,1,2,3...」
当然,你可能会想,如果我就是想要获取最新的值要怎么办的?应用useRef
!
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); }); // ...
此时当咱们疯狂点击后,最终打印进去的全都是最新的count
值。
Effect中的清理又是怎么样的?
思考官网的案例:
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange); }; });
假如第一次渲染的时候props是{id: 10}
,第二次渲染的时候是{id: 20}
。你可能会认为产生了上面的这些事:
- React 革除了
{id: 10}
的effect。 - React 渲染
{id: 20}
的UI。 - React 运行
{id: 20}
的effect。
事实上并不是这样的。React只会在浏览器渲染结束后才会去执行Effect,这并不会阻塞渲染使得你的利用更加晦涩。
Effect的清理实际上也被延后了。上一次Effecet的清理会在UI渲染完结后被执行。
- React 渲染
{id: 20}
的UI。 - 浏览器绘制 咱们在屏幕上看到
{id: 20}
的UI。 - React 革除
{id: 10}
的effect。 - React 运行
{id: 20}
的effect。
通知react如何去比对useEffect
简略来说,你的Effect可能会因为其余状态变动了而造成了不必要的调用。
为了解决这个问题,能够通过依赖数组来通知React
如果以后渲染中的这些依赖项和上一次运行这个effect的时候值一样,因为没有什么须要同步,React会主动跳过这次effect。
如果依赖项设置谬误会怎么样呢?
举个例子,咱们来写一个每秒递增的计数器。在Class组件中,咱们的直觉是:“开启一次定时器,革除也是一次”。
当咱们天经地义地把它用useEffect
的形式翻译,直觉上咱们会设置依赖为[]
。因为“我只想运行一次effect“。
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>;}
在这个例子中,·count
的值永远都不会超过1
。因为定时器只会设置一次,而此时的count
永远都是0,即永远都在执行setCount(0+1)
咱们必须扭转咱们的想法。所谓「依赖数组」是咱们用来通知React该Effect用到了什么状态。在这个例子中,咱们对React扯谎了,通知React说没有用到任何组件内的值。但实际上依赖了count
。
要诚恳的通知React咱们正确的依赖的两种方法
1. 将所有Effect用到的依赖都通知给React
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id);+ }, [count]);
当初依赖正确了,然而会有一个问题,就是每一次执行setCount
的时候,都会导致计数器的革除和新建。这并不合乎咱们的初衷。
2. 批改effect外部的代码以确保它蕴含的值只会在须要的时候产生变更。
咱们不想告知谬误的依赖 - 咱们只是批改effect使得依赖更少。
持续看会下面的示例(将count
传入依赖数组)。咱们须要思考,咱们为什么须要用到count
?咱们想要通过setCount
来更新count
的值。
但其实在这个场景,咱们并不需要获取count
的值,咱们只须要获取上一次的状态进行解决即可。因而能够写成
useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id);}, []);
函数式更新
对于Effect来说,咱们能够用「同步」的思维去了解。
以下面这个例子来说,同步的一个乏味的中央就是将同步信息和状态解耦。相似于setCount(c => c + 1)
这样的更新模式比setCount(count + 1)
传递了更少的信息,因为它不再被以后的count
值“净化”。它只是表白了一种行为(“递增”)。
然而,setCount(c => c + 1)
并不完满,例如咱们有两个状态相互依赖,即下一次状态不仅仅依赖上一次状态,它就不能做到了。
这时候,useReducer
大哥就要出场了。
useReducer
咱们对下面例子做一些批改:
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))} /> </> );}
当初count
的值不仅仅依赖本人上一次的值,还依赖step
状态。
雷同的,目前咱们并没有对React说谎,因而它能够正确运行。但问题在于每一次扭转step
后,计时器都会被销毁重建。
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能须要用useReducer
去替换它们。
当你写相似setSomething(something => ...)
这种代码的时候,兴许就是思考应用reducer的契机。reducer能够让你把组件内产生了什么(actions)和状态如何响应并更新离开表述。
function Counter() { const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); // 更新count }, 1000); return () => clearInterval(id); }, [dispatch]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => { // 更新step dispatch({ type: 'step', step: Number(e.target.value) }); }} /> </> );}function reducer(state, action) { const { count, step } = state; if (action.type === 'tick') { return { count: count + step, step }; } else if (action.type === 'step') { return { count, step: action.step }; } else { throw new Error(); }}
应用useReducer
能够完满解决上述问题。这是为什么呢?
答案在于<span style="color:#d2568c;font-weight:bold;">「React会保障dispatch在组件的申明周期内放弃不变」</span>。所以下面例子中不再须要从新订阅定时器。
(你能够从依赖中去除dispatch
,setState
, 和useRef
包裹的值因为React会确保它们是动态的。不过你设置了它们作为依赖也没什么问题。)
与其在Effect中去获取状态,不如只是dispatch一个action来形容行为,这使得Effect与状态解耦,Effect再也不必关怀具体的状态了~
应用useRuducer真的是在舞弊
当咱们的step
是props传进来的又会怎么呢?咱们还是能够应用useReducer
,但这种状况下咱们的reducer
函数就得写进组件中,因为要获取props。
function Counter({ step }) { const [count, dispatch] = useReducer(reducer, 0); function reducer(state, action) { if (action.type === 'tick') { return state + step; } else { throw new Error(); } } useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]); return <h1>{count}</h1>;}
有的小伙伴可能会问:上一次渲染的reducer
怎么能获取到最新的props呢?
答案是:Effect只是dispatch了行为 - 它会在下一次渲染中去调用reducer,这是就能够获取到最新的props了。留神:「reducer不是在Effect中调用的」
永远要记住useEffect的执行机会
useEffect的执行机会在Dom渲染结束后。而且它是异步执行的,不会阻塞的。因而在上面例子中,会呈现页面闪动的成果。(0 -> 12 -> 2)
export default function FuncCom () { const [counter, setCounter] = useState(0); useEffect(() => { if (counter === 12) { // 为了演示,这里同步设置一个延时函数 500ms delay() setCounter(2) } }); return ( <div style={{ fontSize: '100px' }}> <div onClick={() => setCounter(12)}>{counter}</div> </div> )}
换成了 useLayoutEffect
后,屏幕上只会呈现 0 和 2,这是因为 useLayoutEffect
的同步个性,会在浏览器渲染之前同步更新react DOM 数据,哪怕是屡次的操作,也会在渲染前一次性解决完,再交给浏览器绘制。这样不会导致闪屏景象产生。
将函数放进Effect中
首先抛出问题:函数真的不应该成为依赖项嘛?
咱们来看一个许多程序员都会写的一个案例:
function SearchResults() { const [data, setData] = useState({ hits: [] }); async function fetchData() { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } useEffect(() => { fetchData(); }, []); // Is this okay? // ...}
该代码是能够失常运行的,然而它的拓展性十分差。试想一下,在我的项目代码一直增长下,咱们可能会拆散出很多个很长的函数,可能会有其余的依赖:
function SearchResults() { const [query, setQuery] = useState('react'); // Imagine this function is also long function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } // Imagine this function is also long async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } useEffect(() => { fetchData(); }, []); // ...}
如果咱们遗记更新Effect的依赖数组的话,这不仅仅坑骗了React,而且导致Effect不会同步所依赖的state和props。
一个最简略的解决方案是,如果某些函数仅在effect中调用,你能够把它们的定义移到effect中:。这样的话程序员不须要思考这些“间接依赖”,而且在增加query
状态的时候,意识到Effect依赖于它。
function SearchResults() { const [query, setQuery] = useState('react'); useEffect(() => { // ❇️ get together 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
设计用意就是要强制你去关注数据流的变动,而后决定怎么去让Effect同步!!
但我不能将函数放进Effect中
举个例子,比方某函数被多个Effect应用到了。
首先咱们要明确一点,在组件中定义的函数每一次渲染都是变动的,但这个事实也给咱们带来了问题。比方:
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 // ...}
一方面咱们不想在每一个Effect中都复制雷同的代码,但这又是对React的不诚实~,另一方面假如咱们将getFetchUrl
当作依赖项,但它每一次渲染都在变,咱们的依赖数组就没有施展用途了。
这里有两个解决办法:
- 如果函数并没有用到组件内的值,咱们能够大胆地将其移到组件内部!
- 应用
useCallBack
hook包裹。
useCallBack
实质上只是加了一层依赖查看,使得函数自身只有在须要时才扭转,这样咱们也不须要去掉函数依赖。
当函数用到了组件内的值,比方query
能够通过输入框由用户去扭转,咱们能够这样做:
function SearchResults() { const [query, setQuery] = useState(''); const getFetchUrl = useCallback(() => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, [query]); // ✅ Deps are OK useEffect(() => { const url = getFetchUrl(); // ... Fetch data and do something ... }, [getFetchUrl]); // ✅ Deps are OK useEffect(() => { const url = getFetchUrl(); // ... Fetch data and do something ... }, [getFetchUrl]); // ✅ Deps are OK // ...}
这才是拥抱React数据流和同步思维的最终后果。query
扭转 -> getFetchUrl
扭转 -> Effect从新执行。反之,如果query
不变,则Effect将不会从新执行
函数是数据流的一部分吗?
乏味的是,这种模式在class组件中是行不通的。
class Parent extends Component { state = { query: 'react' }; fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query; // ... Fetch data and do something ... }; render() { return <Child fetchData={this.fetchData} />; }}class Child extends Component { state = { data: null }; componentDidMount() { this.props.fetchData(); } componentDidUpdate(prevProps) { // This condition will never be true if (this.props.fetchData !== prevProps.fetchData) { this.props.fetchData(); } } render() { // ... }}
在class组件中,类办法this.fetchData
是永远不会变的
为了解决这个问题,咱们必须将query
参数传给Child
组件即便它并没有间接应用到query
:
class Child extends Component { componentDidUpdate(prevProps) { // This condition will never be true if (this.props.query !== prevProps.query) { this.props.fetchData(); }}
在class组件中,函数属性自身并不是数据流的一部分。组件的办法中蕴含了可变的this变量导致咱们不能确定无疑地认为它是不变的。因而,即便咱们只须要一个函数,咱们也必须把一堆数据传递上来仅仅是为了做“diff”。咱们无奈晓得传入的this.props.fetchData
是否依赖状态,并且不晓得它依赖的状态是否扭转了。
然而感激useCallback
,它让函数自身也参加进了React数据流,咱们能够说函数的输出变了,那么函数自身就变了。(函数的输出须要咱们来定义)
须要强调的是:当咱们须要将函数传递给子组件并且在子组件的Effect中使用的话,最好应用useCallback
将其包裹。
对于竞态
上面是一个class组件发申请的案例
class Article extends Component { state = { article: null }; componentDidMount() { this.fetchData(this.props.id); } async fetchData(id) { const article = await API.fetchArticle(id); this.setState({ article }); } // ...}
心细的小伙伴可能发现了,下面代码并没有思考到id更新的状况,于是:
componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.fetchData(this.props.id); } }
然而这真的能够完满解决所有问题嘛?
咱们能够试想一下,比方我先申请 {id: 10}
,而后更新到{id: 20}
,但{id: 20}
的申请更先返回。申请更早但返回更晚的状况会谬误地笼罩状态值。这就是「竞态」
最好的权宜之计就是利用一个boolean来去记录以后申请状态。
更多相干常识能够看这篇文章
结尾
我是<span style="color:#87481f;font-weight:bold">「盐焗乳鸽还要香锅」</span>,喜爱我的文章欢送关注噢
- github 链接https://github.com/1360151219
- 博客链接是 strk2.cn
- 掘金账号、知乎账号、简书《盐焗乳鸽还要香锅》
- 思否账号《天天摸鱼真的爽》
本文由mdnice多平台公布