前言 關於Hook的定義官方文檔是這麼說的: Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。 簡單來說,就是在使用函數式組件時能用上state,還有一些生命周期函數等其他的特性。 如果想瞭解Hook怎麼用, " ...
前言
關於Hook的定義官方文檔是這麼說的:
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
簡單來說,就是在使用函數式組件時能用上state,還有一些生命周期函數等其他的特性。
如果想瞭解Hook怎麼用,官方文檔和阮一峰的React Hooks 入門教程都講得很清楚了,我建議直接看官方文檔和阮大神的文章即可。
本篇博客只講為什麼要用React的Hook新特性,以及它解決了什麼問題。
為什麼使用Hook?
讓我們先看看別人怎麼說。
阮大神的文章中給了一個示例代碼:
import React, { Component } from "react";
export default class Button extends Component {
constructor() {
super();
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(() => {
return { buttonText: "Thanks, been clicked!" };
});
}
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
並且提出:
這個組件類僅僅是一個按鈕,但可以看到,它的代碼已經很"重"了。
真實的 React App 由多個類按照層級,一層層構成,複雜度成倍增長。
再加入 Redux,就變得更複雜。
實際上,上面這個代碼的“重”有部分來源於寫法問題,他可能並沒有“重”,讓我們看看下麵這種class寫法:
import React, { Component } from "react";
export default class Button extends Component {
state = {
buttonText: "Click me, please"
}
handleClick = () => {
this.setState(() => {
return { buttonText: "Thanks, been clicked!" };
});
}
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
然後再對比下使用了Hook的函數式組件:
import React, { useState } from "react";
export default function Button() {
const [buttonText, setButtonText] = useState("Click me, please");
function handleClick() {
return setButtonText("Thanks, been clicked!");
}
return <button onClick={handleClick}>{buttonText}</button>;
}
即使是我們簡化過的class寫法,比起Hook的看起來好像也確實“重”了點。
Hook的語法確實簡練了一些,但是這個理由並不是那麼充分。
阮大神同時列舉了Redux 的作者 Dan Abramov 總結了組件類的幾個缺點:
- 大型組件很難拆分和重構,也很難測試。
- 業務邏輯分散在組件的各個方法之中,導致重覆邏輯或關聯邏輯。(這裡我認為阮大神寫的可能有點問題,應該是是各個生命周期方法更為準確)
- 組件類引入了複雜的編程模式,比如 render props 和高階組件。
這三點都是事實,於是有了函數化的組件,但之前的函數化組件沒有state和生命周期,有了Hook那麼就可以解決這個痛點。
而且Hook並不只是這麼簡單,通過自定義Hook,我們可以將原有組件的邏輯提取出來實現復用。
用useEffect解決生命周期導致的重覆邏輯或關聯邏輯
上面舉的幾個缺點,第一點和第三點你可能很容易理解,第二點就不容易理解了,所以我們需要深入到具體的代碼中去理解這句話。
我們看看下麵這段代碼:
import React, { Component } from "react";
export default class Match extends Component {
state={
matchInfo:''
}
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 請求後臺介面獲取賽事信息
// ...
this.setState({
matchInfo:serverResult // serverResult是後臺介面的返回值
})
}
render() {
const { matchInfo } = this.state
return <div>{matchInfo}</div>;
}
}
這樣的代碼在我們的業務中經常會出現,通過修改傳入賽事組件的ID,去改變這個賽事組件的信息。
在上面的代碼中,受生命周期影響,我們需要在載入完畢和Id更新時都寫上重覆的邏輯和關聯邏輯。
所以現在你應該比較好理解這句話:業務邏輯分散在組件的各個生命周期方法之中,導致重覆邏輯或關聯邏輯。
為瞭解決這一點,React提供了useEffect這個鉤子。
但是在講這個之前,我們需要先瞭解到React帶來的一個新的思想:同步。
我們在上面的代碼中所做的實際上就是在把組件內的狀態和組件外的狀態進行同步。
所以在使用Hook之前,我們需要先摒棄生命周期的思想,而用同步的思想去思考這個問題。
現在再讓我們看看改造後的代碼:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事信息
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
看到這個代碼,再對比上面的代碼,你心中第一反應應該就是:簡單。
React.useEffect接受兩個參數,第一個參數是Effect函數,第二個參數是一個數組。
組件載入的時候,執行Effect函數。
組件更新會去判斷數組中的各個值是否變動,如果不變,那麼不會執行Effect函數。
而如果不傳第二個參數,那麼無論載入還是更新,都會執行Effect函數。
順便提一句,這裡有組件載入和更新的生命周期的概念了,那麼也應該是有組件卸載的概念的:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事信息
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
return ()=>{
// 組件卸載後的執行代碼
}
}, [matchId])
return <div>{matchInfo}</div>;
}
}
這個常用於事件綁定解綁之類的。
用自定義Hook解決高階組件
React的高階組件是用來提煉重覆邏輯的組件工廠,簡單一點來說就是個函數,輸入參數為組件A,輸出的是帶有某邏輯的組件A+。
回想一下上面的Match組件,假如這個組件是頁面A的首頁頭部用來展示賽事信息,然後現在頁面B的側邊欄也需要展示賽事信息。
問題就在於頁面A的這塊UI需要用div,而頁面B側邊欄的這塊UI需要用到span。
保證今天早點下班的做法是複製A頁面的代碼到頁面B,然後改下render的UI即可。
保證以後早點下班的做法是使用高階組件,請看下麵的代碼:
import React from "react";
function hocMatch(Component) {
return class Match React.Component {
componentDidMount() {
this.getMatchInfo(this.props.matchId)
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
this.getMatchInfo(this.props.matchId)
}
}
getMatchInfo = (matchId) => {
// 請求後臺介面獲取賽事信息
}
render () {
return (
<Component {...this.props} />
)
}
}
}
const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)
<MatchDiv matchId={1} matchInfo={matchInfo} />
<MatchSpan matchId={1} matchInfo={matchInfo} />
但是實際上有的時候我們的高階組件可能會更複雜,比如react-redux的connect,這就是高階組件的複雜化使用方式。
又比如:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
毫無疑問高階組件能讓我們復用很多邏輯,但是過於複雜的高階組件會讓之後的維護者望而卻步。
而Hook的玩法是使用自定義Hook去提煉這些邏輯,首先看看我們之前使用了Hook的函數式組件:
import React, { Component } from "react";
export default function Match({matchId}) {
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事信息
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return <div>{matchInfo}</div>;
}
然後,自定義Hook:
function useMatch(matchId){
const [ matchInfo, setMatchInfo ] = React.useState('')
React.useEffect(() => {
// 請求後臺介面獲取賽事信息
// ...
setMatchInfo(serverResult) // serverResult是後臺介面的返回值
}, [matchId])
return [matchInfo]
}
接下來,修改原來的Match組件
export default function Match({matchId}) {
const [matchInfo]=useMatch(matchId)
return <div>{matchInfo}</div>;
}
相比高階組件,自定義Hook更加簡單,也更加容易理解。
現在我們再來處理以下這種情況:
hocPage(
hocMatch(
hocDiv(DivComponent)
)
)
我們的代碼將不會出現這種不斷嵌套情況,而是會變成下麵這種:
export default function PageA({matchId}) {
const [pageInfo]=usePage(pageId)
const [matchInfo]=useMatch(matchId)
const [divInfo]=useDiv(divId)
return <ul>
<li>{pageInfo}</li>
<li>{matchInfo}</li>
<li>{divInfo}</li>
</ul>
}
是否需要改造舊的class組件?
現在我們瞭解到了Hook的好,所以就需要去改造舊的class組件。
官方推薦不需要專門為了hook去改造class組件,並且保證將繼續更新class相關功能。
實際上我們也沒有必要專門去改造舊項目中的class組件,因為工作量並不小。
但是我們完全可以在新的項目或者新的組件中去使用它。
總結
Hook是對函數式組件的一次增強,使得函數式組件可以做到class組件的state和生命周期。
Hook的語法更加簡練易懂,消除了class的生命周期方法導致的重覆邏輯代碼,解決了高階組件難以理解和使用困難的問題。
然而Hook並沒有讓函數式組件能做到class組件做不到的事情,它只是讓很多事情變得更加簡單而已。
class組件並不會消失,但hook化的函數式組件將是趨勢。