你真的用對(duì) useEffect 了嗎?
點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)
回復(fù)算法,加入前端編程面試算法每日一題群

最近在公司搬磚的過程中遇到了一個(gè)bug,頁面加載的時(shí)候會(huì)閃現(xiàn)一下,找了很久才發(fā)現(xiàn)是useeffect的依賴項(xiàng)的問題,所以打算寫篇文章總結(jié)一下,希望對(duì)看到文章的你也有所幫助。
1.什么是useEffect?
該 Hook 接收一個(gè)包含命令式、且可能有副作用代碼的函數(shù)。
在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱、設(shè)置定時(shí)器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因?yàn)檫@可能會(huì)產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會(huì)在組件渲染到屏幕之后執(zhí)行。你可以把 effect 看作從 React 的純函數(shù)式世界通往命令式世界的逃生通道。(官方文檔)
這么一看你也許會(huì)有點(diǎn)不明白...
看下面這個(gè)例子:
import React, { useState, useEffect } from 'react';
function Example() {
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>
);
}
復(fù)制代碼
useEffect 做了什么?
通過使用這個(gè) Hook,你可以告訴 React 組件需要在渲染后執(zhí)行某些操作。React 會(huì)保存你傳遞的函數(shù)(我們將它稱之為 “effect”),并且在執(zhí)行 DOM 更新之后調(diào)用它。在這個(gè) effect 中,我們?cè)O(shè)置了 document 的 title 屬性,不過我們也可以執(zhí)行數(shù)據(jù)獲取或調(diào)用其他命令式的 API。
為什么在組件內(nèi)部調(diào)用 useEffect?
將 useEffect 放在組件內(nèi)部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經(jīng)保存在函數(shù)作用域中。Hook 使用了 JavaScript 的閉包機(jī)制,而不用在 JavaScript 已經(jīng)提供了解決方案的情況下,還引入特定的 React API。
useEffect 會(huì)在每次渲染后都執(zhí)行嗎?
是的,默認(rèn)情況下,它在第一次渲染之后和每次更新之后都會(huì)執(zhí)行。(我們稍后會(huì)談到如何控制它。)你可能會(huì)更容易接受 effect 發(fā)生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運(yùn)行 effect 的同時(shí),DOM 都已經(jīng)更新完畢 如果你熟悉 React class 的生命周期函數(shù),你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個(gè)函數(shù)的組合。
2.如何使用useEffect
2.1實(shí)現(xiàn)componentDidMount 的功能
useEffect的第二個(gè)參數(shù)為一個(gè)空數(shù)組,初始化調(diào)用一次之后不再執(zhí)行,相當(dāng)于componentDidMount。
function Demo () {
useEffect(() => {
console.log('hello world')
}, [])
return (
<div>
hello world
</div>
)
}
// 等價(jià)于
class Demo extends Component {
componentDidMount() {
console.log('hello world')
}
render() {
return (
<div>
hello world
</div>
);
}
}
復(fù)制代碼
2.2實(shí)現(xiàn)組合 componentDidMount componentDidUpdate 的功能
當(dāng)useEffect沒有第二個(gè)參數(shù)時(shí),組件的初始化和更新都會(huì)執(zhí)行。
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
// 等價(jià)于
import React, { useState, useEffect } from 'react';
function Example() {
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>
);
}
復(fù)制代碼
2.3實(shí)現(xiàn)組合 componentDidMount componentWillUnmount 的功能
useEffect返回一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在組件卸載時(shí)執(zhí)行。
class Example extends Component {
constructor (props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
this.id = setInterval(() => {
this.setState({count: this.state.count + 1})
}, 1000);
}
componentWillUnmount() {
clearInterval(this.id)
}
render() {
return <h1>{this.state.count}</h1>;
}
}
// 等價(jià)于
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>hello world</h1>
}
復(fù)制代碼
3.useEffect使用的坑
3.1 無限循環(huán)
當(dāng)useEffect的第二個(gè)參數(shù)傳數(shù)組傳一個(gè)依賴項(xiàng),當(dāng)依賴項(xiàng)的值發(fā)生變化,都會(huì)觸發(fā)useEffect執(zhí)行。
請(qǐng)看下面的例子:
App組件顯示了一個(gè)項(xiàng)目列表,狀態(tài)和狀態(tài)更新函數(shù)來自與useState這個(gè)hooks,通過調(diào)用useState,來創(chuàng)建App組件的內(nèi)部狀態(tài)。初始狀態(tài)是一個(gè)object,其中的hits為一個(gè)空數(shù)組,目前還沒有請(qǐng)求后端的接口。
import React, { useState } from 'react';
function App() {
const [data, setData] = useState({ hits: [] });
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
復(fù)制代碼
為了獲取后端提供的數(shù)據(jù),接下來將使用axios來發(fā)起請(qǐng)求,同樣也可以使用fetch,這里會(huì)使用useEffect來隔離副作用。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
復(fù)制代碼
在useEffect中,不僅會(huì)請(qǐng)求后端的數(shù)據(jù),還會(huì)通過調(diào)用setData來更新本地的狀態(tài),這樣會(huì)觸發(fā)view的更新。
但是,運(yùn)行這個(gè)程序的時(shí)候,會(huì)出現(xiàn)無限循環(huán)的情況。useEffect在組件mount時(shí)執(zhí)行,但也會(huì)在組件更新時(shí)執(zhí)行。因?yàn)槲覀冊(cè)诿看握?qǐng)求數(shù)據(jù)之后都會(huì)設(shè)置本地的狀態(tài),所以組件會(huì)更新,因此useEffect會(huì)再次執(zhí)行,因此出現(xiàn)了無限循環(huán)的情況。我們只想在組件mount時(shí)請(qǐng)求數(shù)據(jù)。我們可以傳遞一個(gè)空數(shù)組作為useEffect的第二個(gè)參數(shù),這樣就能避免在組件更新執(zhí)行useEffect,只會(huì)在組件mount時(shí)執(zhí)行。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
復(fù)制代碼
useEffect的第二個(gè)參數(shù)可用于定義其依賴的所有變量。如果其中一個(gè)變量發(fā)生變化,則useEffect會(huì)再次運(yùn)行。如果包含變量的數(shù)組為空,則在更新組件時(shí)useEffect不會(huì)再執(zhí)行,因?yàn)樗粫?huì)監(jiān)聽任何變量的變更。
再看這個(gè)例子:
業(yè)務(wù)場(chǎng)景:需要在頁面一開始時(shí)得到一個(gè)接口的返回值,取調(diào)用另一個(gè)接口。
我的思路是,先設(shè)置這個(gè)接口的返回值為data=[], 等到數(shù)據(jù)是再去請(qǐng)求另一個(gè)接口,即data作為useEffect的第二個(gè)參數(shù)傳入。
但是不知道為什么會(huì)造成死循環(huán),拿不到我們想要的結(jié)果。
直到在官網(wǎng)看到這個(gè)例子:
知道useEffect會(huì)比較前一次渲染和后一次渲染的值,然后我就在想,如果我所設(shè)置的data=[],那么即使我后一次渲染的data也為[],那么[]===[]為false,所以才會(huì)造成useEffect會(huì)一直不停的渲染,所以我把data的初始值改為undefined,試了一下果然可以。
結(jié)論:useEffect的不作為componentDidUnmount的話,傳入第二個(gè)參數(shù)時(shí)一定注意:第二個(gè)參數(shù)不能為引用類型,引用類型比較不出來數(shù)據(jù)的變化,會(huì)造成死循環(huán)
3.2使用async await 時(shí)的報(bào)錯(cuò)
在代碼中,我們使用async / await從第三方API獲取數(shù)據(jù)。如果你對(duì)async/await熟悉的話,你會(huì)知道,每個(gè)async函數(shù)都會(huì)默認(rèn)返回一個(gè)隱式的promise。但是,useEffect不應(yīng)該返回任何內(nèi)容。這就是為什么會(huì)在控制臺(tái)日志中看到以下警告:
Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect
這就是為什么不能直接在useEffect中使用async函數(shù),因此,我們可以不直接調(diào)用async函數(shù),而是像下面這樣:
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
復(fù)制代碼
4.useEffect在實(shí)戰(zhàn)中的應(yīng)用
4.1 響應(yīng)更新
很多情況下,我們需要響應(yīng)用戶的輸入,然后再請(qǐng)求。這個(gè)時(shí)候我們會(huì)引入一個(gè)input框,監(jiān)聽query值的變化:
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
復(fù)制代碼
有個(gè)query值,已經(jīng)更新query的邏輯,還需要將這個(gè)query值傳遞給后臺(tái),這個(gè)操作會(huì)在useEffect中進(jìn)行
前面我們說了,目前的useEffect只會(huì)在組件mount時(shí)執(zhí)行,并且useEffect的第二個(gè)參數(shù)是依賴的變量,一旦這個(gè)依賴的變量變動(dòng),useEffect就會(huì)重新執(zhí)行,所以我們需要添加query為useEffect的依賴:
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://localhost/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
...
);
}
復(fù)制代碼
一旦更改了query值,就可以重新獲取數(shù)據(jù)。但這會(huì)帶來另一個(gè)問題:query的任何一次變動(dòng)都會(huì)請(qǐng)求后端,這樣會(huì)帶來比較大的訪問壓力。這個(gè)時(shí)候我們需要引入一個(gè)按鈕,點(diǎn)擊這個(gè)按鈕再發(fā)起請(qǐng)求。
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://localhost/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setSearch(query)}>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
復(fù)制代碼
可以看到上面我們添加了一個(gè)新的按鈕,然后創(chuàng)建新的組件state:search。每次點(diǎn)擊按鈕時(shí),會(huì)把search的值設(shè)置為query,這個(gè)時(shí)候我們需要修改useEffect中的依賴項(xiàng)為search,這樣每次點(diǎn)擊按鈕,search值變更,useEffect就會(huì)重新執(zhí)行,避免不必要的變更:
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://localhost/api/v1/search?query=${search}`,
);
setData(result.data);
};
fetchData();
}, [search]);
return (
...
);
}
export default App;
復(fù)制代碼
此外,search state的初始狀態(tài)設(shè)置為與query state 相同的狀態(tài),因?yàn)榻M件首先會(huì)在mount時(shí)獲取數(shù)據(jù)。所以簡單點(diǎn),直接將的要請(qǐng)求的后端URL設(shè)置為search state的初始值。
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://localhost/api/v1/search?query=redux',
);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData(result.data);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://localhost/api/v1/search?query=${query}`)
}
>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
復(fù)制代碼
4.2 如何處理Loading和Error
良好的用戶體驗(yàn)是需要在請(qǐng)求后端數(shù)據(jù),數(shù)據(jù)還沒有返回時(shí)展現(xiàn)loading的狀態(tài),因此,我們還需要添加一個(gè)loading的state
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://localhost/api/v1/search?query=${query}`)
}
>
Search
</button>
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
復(fù)制代碼
在useEffect中,請(qǐng)求數(shù)據(jù)前將loading置為true,在請(qǐng)求完成后,將loading置為false。我們可以看到useEffect的依賴數(shù)據(jù)中并沒有添加loading,這是因?yàn)椋覀儾恍枰賚oading變更時(shí)重新調(diào)用useEffect。請(qǐng)記住:只有某個(gè)變量更新后,需要重新執(zhí)行useEffect的情況,才需要將該變量添加到useEffect的依賴數(shù)組中。
loading處理完成后,還需要處理錯(cuò)誤,這里的邏輯是一樣的,使用useState來創(chuàng)建一個(gè)新的state,然后在useEffect中特定的位置來更新這個(gè)state。由于我們使用了async/await,可以使用一個(gè)大大的try-catch:
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://localhost/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return (......)
復(fù)制代碼
每次useEffect執(zhí)行時(shí),將會(huì)重置error;在出現(xiàn)錯(cuò)誤的時(shí)候,將error置為true;在正常請(qǐng)求完成后,將error置為false。
4.3 處理表單
通常,我們不僅會(huì)用到上面的輸入框和按鈕,更多的時(shí)候是一張表單,所以也可以在表單中使用useEffect來處理數(shù)據(jù)請(qǐng)求,邏輯是相同的:
function App() {
...
return (
<Fragment>
<form
onSubmit={() =>
setUrl(`http://localhost/api/v1/search?query=${query}`)
}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
...
</Fragment>
);
}
復(fù)制代碼
上面的例子中,提交表單的時(shí)候,會(huì)觸發(fā)頁面刷新;就像通常的做法那樣,還需要阻止默認(rèn)事件,來阻止頁面的刷新。
function App() {
...
const doFetch = () => {
setUrl(`http://localhost/api/v1/search?query=${query}`);
};
return (
<Fragment>
<form onSubmit={event => {
doFetch();
event.preventDefault();
}}>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
...
</Fragment>
);
}
復(fù)制代碼
4.4 自定義hooks
我們可以看到上面的組件,添加了一系列hooks和邏輯之后,已經(jīng)變得非常的龐大。那這時(shí)候我們?cè)趺刺幚砟兀縣ooks的一個(gè)非常的優(yōu)勢(shì),就是能夠很方便的提取自定義的hooks。這個(gè)時(shí)候,我們就能把上面的一大堆邏輯抽取到一個(gè)單獨(dú)的hooks中,方便復(fù)用和解耦。
function useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'http://localhost/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
const doFetch = () => {
setUrl(`http://localhost/api/v1/search?query=${query}`);
};
return { data, isLoading, isError, doFetch };
}
復(fù)制代碼
在自定義的hooks抽離完成后,引入到組件中。
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
...
</Fragment>
);
}
復(fù)制代碼
然后我們需要在form組件中設(shè)定初始的后端URL
const useHackerNewsApi = () => {
...
useEffect(
...
);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
<form
onSubmit={event => {
doFetch(
`http://localhost/api/v1/search?query=${query}`,
);
event.preventDefault();
}}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
...
</Fragment>
);
}
復(fù)制代碼
4.5使用useReducer整合邏輯
到目前為止,我們已經(jīng)使用了各種state hooks來管理數(shù)據(jù),包括loading、error、data等狀態(tài)。但是我們可以看到,這三個(gè)有關(guān)聯(lián)的狀態(tài)確是分散的,它們通過分離的useState來創(chuàng)建,為了有關(guān)聯(lián)的狀態(tài)整合到一起,我們需要用到useReducer。
如果你寫過redux,那么將會(huì)對(duì)useReducer非常的熟悉,可以把它理解為一個(gè)輕量額redux。useReducer 返回一個(gè)狀態(tài)對(duì)象和一個(gè)可以改變狀態(tài)對(duì)象的dispatch函數(shù)。跟redux類似的,dispatch函數(shù)接受action作為參數(shù),action包含type和payload屬性。我們看一個(gè)簡單的例子吧:
import React, {
Fragment,
useState,
useEffect,
useReducer,
} from 'react';
import axios from 'axios';
const dataFetchReducer = (state, action) => {
...
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
};
復(fù)制代碼
useReducer將reducer函數(shù)和初始狀態(tài)對(duì)象作為參數(shù)。在我們的例子中,data,loading和error狀態(tài)的初始值與useState創(chuàng)建時(shí)一致,但它們已經(jīng)整合到一個(gè)由useReducer創(chuàng)建對(duì)象,而不是多個(gè)useState創(chuàng)建的狀態(tài)。
const dataFetchReducer = (state, action) => {
...
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData();
}, [url]);
...
};
復(fù)制代碼
在獲取數(shù)據(jù)時(shí),可以調(diào)用dispatch函數(shù),將信息發(fā)送給reducer。使用dispatch函數(shù)發(fā)送的參數(shù)為object,具有type屬性和可選payload的屬性。type屬性告訴reducer需要應(yīng)用哪個(gè)狀態(tài)轉(zhuǎn)換,并且reducer可以使用payload來創(chuàng)建新的狀態(tài)。在這里,我們只有三個(gè)狀態(tài)轉(zhuǎn)換:發(fā)起請(qǐng)求,請(qǐng)求成功,請(qǐng)求失敗。
在自定義hooks的末尾,state像以前一樣返回,但是因?yàn)槲覀兡玫降氖且粋€(gè)狀態(tài)對(duì)象,而不是以前那種分離的狀態(tài),所以需要將狀態(tài)對(duì)象解構(gòu)之后再返回。這樣,調(diào)用useDataApi自定義hooks的人仍然可以訪問data,isLoading 和 isError這三個(gè)狀態(tài)。
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
復(fù)制代碼
接下來添加reducer函數(shù)的實(shí)現(xiàn)。它需要三種不同的狀態(tài)轉(zhuǎn)換FETCH_INIT,F(xiàn)ETCH_SUCCESS和FETCH_FAILURE。每個(gè)狀態(tài)轉(zhuǎn)換都需要返回一個(gè)新的狀態(tài)對(duì)象。讓我們看看如何使用switch case語句實(shí)現(xiàn)它:
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
}
};
復(fù)制代碼
4.6取消數(shù)據(jù)請(qǐng)求
React中的一種很常見的問題是:如果在組件中發(fā)送一個(gè)請(qǐng)求,在請(qǐng)求還沒有返回的時(shí)候卸載了組件,這個(gè)時(shí)候還會(huì)嘗試設(shè)置這個(gè)狀態(tài),會(huì)報(bào)錯(cuò)。我們需要在hooks中處理這種情況,可以看下是怎樣處理的:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
復(fù)制代碼
我們可以看到這里新增了一個(gè)didCancel變量,如果這個(gè)變量為true,不會(huì)再發(fā)送dispatch,也不會(huì)再執(zhí)行設(shè)置狀態(tài)這個(gè)動(dòng)作。這里我們?cè)趗seEffe的返回函數(shù)中將didCancel置為true,在卸載組件時(shí)會(huì)自動(dòng)調(diào)用這段邏輯。也就避免了再卸載的組件上設(shè)置狀態(tài)。
5.useEffect 與 useLayoutEffect

useEffect 在全部渲染完畢后才會(huì)執(zhí)行 useLayoutEffect 會(huì)在 瀏覽器 layout 之后,painting 之前執(zhí)行 其函數(shù)簽名與 useEffect 相同,但它會(huì)在所有的 DOM 變更之后同步調(diào)用 effect 可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染 在瀏覽器執(zhí)行繪制之前 useLayoutEffect 內(nèi)部的更新計(jì)劃將被同步刷新 盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視圖更新
function LayoutEffect() {
const [color, setColor] = useState('red');
useLayoutEffect(() => {
alert(color);
});
useEffect(() => {
console.log('color', color);
});
return (
<>
<div id="myDiv" style={{ background: color }}>顏色</div>
<button onClick={() => setColor('red')}>紅</button>
<button onClick={() => setColor('yellow')}>黃</button>
<button onClick={() => setColor('blue')}>藍(lán)</button>
</>
);
}
復(fù)制代碼
useEffect優(yōu)勢(shì)
useEffect 在渲染結(jié)束時(shí)執(zhí)行,所以不會(huì)阻塞瀏覽器渲染進(jìn)程,所以使用 Function Component 寫的項(xiàng)目一般都有用更好的性能。
自然符合 React Fiber 的理念,因?yàn)?Fiber 會(huì)根據(jù)情況暫停或插隊(duì)執(zhí)行不同組件的 Render,如果代碼遵循了 Capture Value 的特性,在 Fiber 環(huán)境下會(huì)保證值的安全訪問,同時(shí)弱化生命周期也能解決中斷執(zhí)行時(shí)帶來的問題。
useEffect 不會(huì)在服務(wù)端渲染時(shí)執(zhí)行。由于在 DOM 執(zhí)行完畢后才執(zhí)行,所以能保證拿到狀態(tài)生效后的 DOM 屬性。
6.useEffect源碼解析
首先我們要牢記 effect hook 的一些屬性:
它們?cè)阡秩緯r(shí)被創(chuàng)建,但是在瀏覽器繪制后運(yùn)行。 如果給出了銷毀指令,它們將在下一次繪制前被銷毀。 它們會(huì)按照定義的順序被運(yùn)行。
于是就應(yīng)該有另一個(gè)隊(duì)列來保存這些 effect hook,并且還要能夠在繪制后被定位到。通常來說,應(yīng)該是 fiber 保存包含了 effect 節(jié)點(diǎn)的隊(duì)列。每個(gè) effect 節(jié)點(diǎn)都是一個(gè)不同的類型,并能在適當(dāng)?shù)臓顟B(tài)下被定位到:
在修改之前調(diào)用 getSnapshotBeforeUpdate() 實(shí)例。
運(yùn)行所有插入、更新、刪除和 ref 的卸載。
運(yùn)行所有生命周期函數(shù)和 ref 回調(diào)函數(shù)。生命周期函數(shù)會(huì)在一個(gè)獨(dú)立的通道中運(yùn)行,所以整個(gè)組件樹中所有的替換、更新、刪除都會(huì)被調(diào)用。這個(gè)過程還會(huì)觸發(fā)任何特定于渲染器的初始 effect hook。
useEffect() hook 調(diào)度的 effect —— 也被稱為“被動(dòng) effect”,它基于這部分代碼。
hook effect 將會(huì)被保存在 fiber 一個(gè)稱為 updateQueue 的屬性上,每個(gè) effect 節(jié)點(diǎn)都有如下的結(jié)構(gòu):
tag —— 一個(gè)二進(jìn)制數(shù)字,它控制了 effect 節(jié)點(diǎn)的行為 create —— 繪制之后運(yùn)行的回調(diào)函數(shù) destroy —— 它是 create() 返回的回調(diào)函數(shù),將會(huì)在初始渲染前運(yùn)行 inputs —— 一個(gè)集合,該集合中的值將會(huì)決定一個(gè) effect 節(jié)點(diǎn)是否應(yīng)該被銷毀或者重新創(chuàng)建 next —— 它指向下一個(gè)定義在函數(shù)組件中的 effect 節(jié)點(diǎn)
除了 tag 屬性,其他的屬性都很簡明易懂。如果你對(duì) hook 很了解,你應(yīng)該知道,React 提供了一些特殊的 effect hook:比如 useMutationEffect() 和 useLayoutEffect()。這兩個(gè) effect hook 內(nèi)部都使用了 useEffect(),實(shí)際上這就意味著它們創(chuàng)建了 effect hook,但是卻使用了不同的 tag 屬性值。這個(gè) tag 屬性值是由二進(jìn)制的值組合而成(詳見源碼):
const NoEffect = /* */ 0b00000000;
const UnmountSnapshot = /* */ 0b00000010;
const UnmountMutation = /* */ 0b00000100;
const MountMutation = /* */ 0b00001000;
const UnmountLayout = /* */ 0b00010000;
const MountLayout = /* */ 0b00100000;
const MountPassive = /* */ 0b01000000;
const UnmountPassive = /* */ 0b10000000;
復(fù)制代碼
復(fù)制代碼React 支持的 hook effect 類型 這些二進(jìn)制值中最常用的情景是使用管道符號(hào)(|)連接,將比特相加到單個(gè)某值上。然后我們就可以使用符號(hào)(&)檢查某個(gè) tag 屬性是否能觸發(fā)一個(gè)特定的行為。如果結(jié)果是非零的,就表示可以。
const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)
復(fù)制代碼
復(fù)制代碼如何使用 React 的二進(jìn)制設(shè)計(jì)模式的示例 這里是 React 支持的 hook effect,以及它們的 tag 屬性(詳見源碼):
Default effect?——?UnmountPassive | MountPassive. Mutation effect?——?UnmountSnapshot | MountMutation. Layout effect?——?UnmountMutation | MountLayout.
以及這里是 React 如何檢查行為觸發(fā)的(詳見源碼):
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
}
復(fù)制代碼
源碼節(jié)選 所以,基于我們剛才學(xué)習(xí)的關(guān)于 effect hook 的知識(shí),我們可以實(shí)際操作,從外部向 fiber 插入一些 effect:
function injectEffect(fiber) {
const lastEffect = fiber.updateQueue.lastEffect
const destroyEffect = () => {
console.log('on destroy')
}
const createEffect = () => {
console.log('on create')
return destroy
}
const injectedEffect = {
tag: 0b11000000,
next: lastEffect.next,
create: createEffect,
destroy: destroyEffect,
inputs: [createEffect],
}
lastEffect.next = injectedEffect
}
const ParentComponent = (
<ChildComponent ref={injectEffect} />
)
復(fù)制代碼
這就是我對(duì)于useEffect的一點(diǎn)總結(jié),筆者很菜,如果有錯(cuò)誤歡迎指正。
參考文章:
juejin.cn/post/684490… blog.csdn.net/sinat_17775…
關(guān)于本文
作者:Angus安格斯
https://juejin.cn/post/6952509261519781918
