有了 react Hooks 的加持,媽媽再也不用擔心函數元件記不住狀态
過去,react 中的函數元件都被稱為無狀态函數式元件(stateless functional component),這是因為函數元件沒有辦法擁有自己的狀态,隻能根據 Props 來渲染 UI ,其性質就相當于是類元件中的 render 函數,雖然結構簡單明了,但是作用有限。
但自從 React Hooks 橫空出世,函數元件也擁有了儲存狀态的能力,而且也逐漸能夠覆寫到類元件的應用場景,是以可以說 React Hooks 就是未來 React 發展的方向。
React Hooks 解決了什麼問題
複雜的元件難以分拆
我們知道元件化的思想就是将一個複雜的頁面/大元件,按照不同層次,逐漸抽象并拆分成功能更純粹的小元件,這樣一方面可以減少代碼耦合,另外一方面也可以更好地複用代碼;但實際上,在使用 React 的類元件時,往往難以進一步分拆複雜的元件,這是因為邏輯是有狀态的,如果強行分拆,會令代碼複雜性急劇上升;如使用 HOC 和 Render Props 等設計模式,這會形成“嵌套地獄”,使我們的代碼變得晦澀難懂。
狀态邏輯複雜,給單元測試造成障礙
這其實也是上一點的延續:要給一個擁有衆多狀态邏輯的元件寫單元測試,無疑是一件令人崩潰的事情,因為需要編寫大量的測試用例來覆寫代碼執行路徑。
元件生命周期繁複
對于類元件,我們需要在元件提供的生命周期鈎子中處理狀态的初始化、資料擷取、資料更新等操作,處理起來本身邏輯就比較複雜,而且各種“副作用”混在一起也使人頭暈目眩,另外還很可能忘記在元件狀态變更/元件銷毀時消除副作用。
React Hooks 就是來解決以上這些問題的
- 針對狀态邏輯分拆複用難的問題:其實并不是 React Hooks 解決的,函數這一形式本身就具有邏輯簡單、易複用等特性。
- 針對元件生命周期繁複的問題:React Hooks 屏蔽了生命周期這一概念,一切的邏輯都是由狀态驅動,或者說由資料驅動的,那麼了解、處理起來就簡單多了。
利用自定義 Hooks 捆綁封裝邏輯與相關 state
我認為 React Hooks 的亮點不在于 React 官方提供的那些 API ,那些 API 隻是一些基礎的能力;其亮點還是在于自定義 Hooks —— 一種封裝複用的設計模式。
例如,一個頁面上往往有很多狀态,這些狀态分别有各自的處理邏輯,如果用類元件的話,這些狀态和邏輯都會混在一起,不夠直覺:
class Com extends React.Component {
state = {
a: 1,
b: 2,
c: 3,
}
componentDidMount() {
handleA()
handleB()
handleC()
}
}
而使用 React Hooks 後,我們可以把狀态和邏輯關聯起來,分拆成多個自定義 Hooks ,代碼結構就會更清晰:
function useA() {
const [a, setA] = useState(1)
useEffect(() => {
handleA()
}, [])
return a
}
function useB() {
const [b, setB] = useState(2)
useEffect(() => {
handleB()
}, [])
return b
}
function useC() {
const [c, setC] = useState(3)
useEffect(() => {
handleC()
}, [])
return c
}
function Com() {
const a = useA()
const b = useB()
const c = useC()
}
我們除了可以利用自定義 Hooks 來拆分業務邏輯外,還可以拆分成複用價值更高的通用邏輯,比如說目前比較流行的 Hooks 庫:react-use;另外,React 生态中原來的很多庫,也開始提供 Hooks API ,如 react-router 。
忘記元件生命周期吧
React 提供了大量的元件生命周期鈎子,雖然在日常業務開發中,用到的不多,但光是 componentDidUpdate 和 componentWillUnmount 就讓人很頭痛了,一不留神就忘記處理 props 更新群組件銷毀需要處理副作用的場景,這不僅會留下肉眼可見的 bug ,還會留下一些記憶體洩露的隐患。
類 MVVM 架構講究的是資料驅動,而生命周期這種設計模式,就明顯更偏向于傳統的事件驅動模型;當我們引入 React Hooks 後,資料驅動的特性能夠變得更純粹。
處理 props 更新
下面我們以一個非常典型的清單頁面來舉個例子:
class List extends Component {
state = {
data: []
}
fetchData = (id, authorId) => {
// 請求接口
}
componentDidMount() {
this.fetchData(this.props.id, this.props.authorId)
// ...其它不相關的初始化邏輯
}
componentDidUpdate(prevProps) {
if (
this.props.id !== prevProps.id ||
this.props.authorId !== prevProps.authorId // 别漏了!
) {
this.fetchData(this.props.id, this.props.authorId)
}
// ...其它不相關的更新邏輯
}
render() {
// ...
}
}
上面這段代碼有3個問題:
- 需要同時在兩個生命周期裡執行幾乎相同的邏輯。
- 在判斷是否需要更新資料的時候,容易漏掉依賴的條件。
- 每個生命周期鈎子裡,會散落大量不相關的邏輯代碼,違反了高内聚的原則,影響閱讀代碼的連貫性。
如果改成用 React Hooks 來實作,問題就能得到很大程度上的解決了:
function List({ id, authorId }) {
const [data, SetData] = useState([])
const fetchData = (id, authorId) => {}
useEffect(() => {
fetchData(id, authorId)
}, [id, authorId])
}
改用 React Hooks 後:
- 我們不需要考慮生命周期,我們隻需要把邏輯依賴的狀态都丢進依賴清單裡, React 會幫我們判斷什麼時候該執行的。
- React 官方提供了 eslint 的插件來檢查依賴項清單是否完整。
- 我們可以使用多個 useEffect ,或者多個自定義 Hooks 來區分開多個無關聯的邏輯代碼段,保障高内聚特性。
處理副作用
最常見的副作用莫過于綁定 DOM 事件:
class List extends React.Component {
handleFunc = () => {}
componentDidMount() {
window.addEventListener('scroll', this.handleFunc)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleFunc)
}
}
這塊也還是會有上述說的,影響高内聚的問題,改成 React Hooks :
function List() {
useEffect(() => {
window.addEventListener('scroll', this.handleFunc)
}, () => {
window.removeEventListener('scroll', this.handleFunc)
})
}
而且比較絕的是,除了在元件銷毀的時候會觸發外,在依賴項變化的時候,也會執行清除上一輪的副作用。
利用 useMemo 做局部性能優化
在使用類元件的時候,我們需要利用 componentShouldUpdate 這個生命周期鈎子來判斷目前是否需要重新渲染,而改用 React Hooks 後,我們可以利用 useMemo 來判斷是否需要重新渲染,達到局部性能優化的效果:
function List(props) => {
useEffect(() => {
fetchData(props.id)
}, [props.id])
return useMemo(() => (
// ...
), [props.id])
}
在上面這段代碼中,我們看到最終渲染的内容是依賴于props.id,那麼隻要props.id不變,即便其它 props 再怎麼辦,該元件也不會重新渲染。
vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com
依靠 useRef 擺脫閉包
在我們剛開始使用 React Hooks 的時候,經常會遇到這樣的場景:在某個事件回調中,需要根據目前狀态值來決定下一步執行什麼操作;但我們發現事件回調中拿到的總是舊的狀态值,而不是最新狀态值,這是怎麼回事呢?
function Counter() {
const [count, setCount] = useState(0);
const log = () => {
setCount(count + 1);
setTimeout(() => {
console.log(count);
}, 3000);
};
return (
<button onClick={log}>報數</button>
);
}
/*
如果我們在三秒内連續點選三次,那麼count的值最終會變成 3,而随之而來的輸出結果是?
0
1
2
*/
類元件是怎麼實作的?
借助 useRef 共享修改
通過useRef建立的對象,其值隻有一份,而且在所有 Rerender 之間共享。
function Counter() {
const count = useRef(0);
const log = () => {
count.current++;
setTimeout(() => {
console.log(count.current);
}, 3000);
};
return (
<button onClick={log}>報數</button>
);
}
/*
3
3
3
*/