React Hooks基本使用詳解
React Hooks基本使用詳解
?Hooks let you use state and other React features without writing a class
Hooks可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性
?
「一、類組件不足:」
狀態(tài)難以復(fù)用(渲染屬性Render Props、高階組件HOC、Mixin、Hooks可以解決這個問題)在hooks出來之前,常見的代碼重用方式是 HOC 和render props,這兩種方式帶來的問題是:你需要解構(gòu)自己的組件,同時會帶來很深的組件嵌套 Render Props HOC(調(diào)用時候比render props方便) 
? 
image-20210912214946082 
image-20210912215446423 
image-20210912215522321 缺少復(fù)用機制 渲染屬性Render Props和高階組件HOC導(dǎo)致層級冗余 趨向復(fù)雜難以維護 生命周期函數(shù)經(jīng)常包含不相干邏輯 相干邏輯被打散在不同生命周期,理解代碼邏輯也很吃力 this指向困擾 內(nèi)聯(lián)函數(shù)過度創(chuàng)建新句柄(每次都是新的,會重新觸發(fā),導(dǎo)致子組件不停渲染) 類成員函數(shù)不能保證this


二、「Hooks優(yōu)勢(優(yōu)化類組件的三大問題):」
函數(shù)組件無this問題(都在函數(shù)內(nèi)部,沒有實例化的概念) 自定義Hook方便復(fù)用狀態(tài)邏輯 副作用的關(guān)注點分離(不是發(fā)生在數(shù)據(jù)向視圖轉(zhuǎn)化之中,都是在之外的。例如:發(fā)起網(wǎng)絡(luò)請求、訪問原型上的DOM元素、寫本地持久化緩存、綁定解綁事件都是數(shù)據(jù)渲染視圖之外的。這些一般都是放在生命周期中的。useEffect都是在每次渲染完成之后調(diào)用)


研發(fā)挑戰(zhàn):

一開始的時候覺得 hooks 非常地神秘,寫慣了 class 式的組件后,我們的思維就會定格在那里,生命周期,state,this等的使用。因此會以 class 編寫的模式去寫函數(shù)式組件,導(dǎo)致我們一次又一次地爬坑,接下來我們就開始我們的實現(xiàn)方式講解。(提示:以下是都只是一種簡單的模擬方法,與實際有一些差別,但是核心思想是一致的)
三、Hooks的API
1.使用 State Hook
演示地址:2.useState class類 Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useClassState.js
import React, { Component } from "react";
class UseClassState extends Component {
state = {
count: 0
};
render() {
const { count } = this.state;
return (
<button
type="button"
onClick={() => {
this.setState({
count: count + 1
});
}}
>
Click({count})
</button>
);
}
}
export default UseClassState;
以上代碼很好理解,點擊按鈕讓 count 值加 1。
接下來我們使用 useState 來實現(xiàn)上述功能。
栗子:1.useState Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useStateHook.js
import React, {useState} from 'react'
function App () {
//返回兩個參數(shù) 一個是當(dāng)前的狀態(tài),還有一個是修改當(dāng)前狀態(tài)的函數(shù)
const [count, setCount] = useState(0)
// 動態(tài)傳入?yún)?shù) //延遲初始化,提高效率,只會在初始化時候執(zhí)行一次
// const [count,setCount] = useState(()=>{return props.defaultCount||0})
onst [name, setName] = useState('小圓臉兒')
return (
<button type="button"
onClick={() => {setCount(count + 1) }}
>Click({count})</button>
)
}
在這里,useState 就是一個 Hook。通過在函數(shù)組件里調(diào)用它來給組件添加一些內(nèi)部 state,React 會在重復(fù)渲染時保留這個 state。
useState 會返回一對值**:當(dāng)前狀態(tài)**和一個讓你更新它的函數(shù)。你可以在事件處理函數(shù)中或其他一些地方調(diào)用這個函數(shù)。它類似 class 組件的 this.setState,但是它不會把新的 state 和舊的 state 進行合并。useState 唯一的參數(shù)就是初始 state。
useState 讓代碼看起來簡潔了,但是我們可能會對組件中,直接調(diào)用 useState 返回的狀態(tài)會有些懵。既然 useState 沒有傳入任何的環(huán)境參數(shù),它怎么知道要返回的的是 count 的呢?而且還是這個組件的 count 不是其它組件的 count?
初淺的理解: useState 確實不知道我們要返回的 count,但其實也不需要知道,它只要返回一個變量就行了。「數(shù)組解構(gòu)」的語法讓我們在調(diào)用 useState 時可以給 state 變量取不同的名字。
useState 怎么知道要返回當(dāng)前組件的 state?
因為 JavaScript 是單線程的。在 useState 被調(diào)用時,它只能在唯一一個組件的上下文中。
有人可能會問,如果一個組件內(nèi)有多個 usreState,那 useState 怎么知道哪一次調(diào)用返回哪一個 state 呢?
這個就是按照第一次運行的次序來順序來返回的。
接著上面的例子我們在聲明一個 useState:
...
const [count, setScount] = useState(0)
const [name, setName] = useState('小圓臉兒')
...
然后我們就可以斷定,以后APP組件每次渲染的時候,useState 第一次調(diào)用一定是返回 count,第二次調(diào)用一定是返回 name。
為了防止我們使用 useState 不當(dāng),React 提供了一個 ESlint 插件幫助我們檢查。
eslint-plugin-react-hooks
「優(yōu)化點」
通過上述我們知道 useState 有個默認(rèn)值,因為是默認(rèn)值,所以在不同的渲染周期去傳入不同的值是沒有意義的,只有第一次傳入的才有效。如下所示:
...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...
state 的默認(rèn)值是基于 props,在 APP 組件每次渲染的時候 const defaultCount = props.defaultCount || 0 都會運行一次,如果它復(fù)雜度比較高的話,那么浪費的資料肯定是可觀的。
useState 支持傳入函數(shù),來延遲初始化:
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
2.使用Effect Hooks
Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作(綁定事件、發(fā)送請求、訪問DOM元素)。數(shù)據(jù)獲取,設(shè)置訂閱以及手動更改 React 組件中的 DOM 都屬于副作用。不管你知不知道這些操作,或是"副作用"這個名字,應(yīng)該都在組件中使用過它們。
副作用的時機
Mount 之后 對應(yīng) componentDidMountUpdate 之后 對應(yīng) componentDidUpdateUnmount 之前 對應(yīng) componentWillUnmount
現(xiàn)在使用 useEffect 就可以覆蓋上述的情況。
為什么一個 useEffect 就能涵蓋 Mount,Update,Unmount 等場景呢?
useEffect 標(biāo)準(zhǔn)上是在組件每次渲染之后調(diào)用,并且會根據(jù)自定義狀態(tài)來決定是否調(diào)用還是不調(diào)用。
第一次調(diào)用就相當(dāng)于componentDidMount,后面的調(diào)用相當(dāng)于 componentDidUpdate。useEffect 還可以返回另一個回調(diào)函數(shù),這個函數(shù)的執(zhí)行時機很重要。作用是清除上一次副作用遺留下來的狀態(tài)。

比如一個組件在第三次,第五次,第七次渲染后執(zhí)行了 useEffect 邏輯,那么回調(diào)函數(shù)就會在第四次,第六次和第八次渲染之前執(zhí)行。嚴(yán)格來講,是在前一次的渲染視圖清除之前。如果 useEffect 是在第一次調(diào)用的,那么它返回的回調(diào)函數(shù)就只會在組件卸載之前調(diào)用了,也就是componentWillUnmount 。

?如果你熟悉 React class 的生命周期函數(shù),你可以把 useEffect Hook 看做
?componentDidMount,componentDidUpdate和componentWillUnmount這三個函數(shù)的組合。
在線舉栗子說明一下:4.class類 useClassEffect Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useClassEffect.js
import React, { Component } from "react";
class UseClassEffect extends Component {
state = {
count: 0,
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
};
onResize = () => {
this.setState({
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
});
};
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/resize_event
componentDidMount() {
document.title = this.state.count;
window.addEventListener("resize", this.onResize, false);
}
componentWillMount() {
window.removeEventListener("resize", this.onResize, false);
}
componentDidUpdate() {
document.title = this.state.count;
}
render() {
const { count, size } = this.state;
return (
<button
type="button"
onClick={() => {
this.setState({ count: count + 1 });
}}
>
Click({count}) size: {size.width}x{size.height}
</button>
);
}
}
export default UseClassEffect;
對于生命周期我們在復(fù)習(xí)一遍:

上面主要做的就是網(wǎng)頁 title 顯示count 值,并監(jiān)聽網(wǎng)頁大小的變化。這里用到了componentDidMount,componentDidUpdate 等副作用,因為第一次掛載我們需要把初始值給 title, 當(dāng) count 變化時,把變化后的值給它 title,這樣 title 才能實時的更新。
「注意,我們需要在兩個生命周期函數(shù)中編寫重復(fù)的代碼。」
這邊我們?nèi)菀壮鲥e的地方就是在組件結(jié)束之后要「記住銷毀事件的注冊」,不然會導(dǎo)致資源的泄漏?,F(xiàn)在我們把 App 組件的副作用用 useEffect 實現(xiàn)。
在線栗子5.useEffect Demo:useEffectHook:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useEffectHook.js
import React, {useState, useEffect } from "react";
function UseEffectHook(props) {
const [count, setCount] = useState(0);
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = () => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
};
// 第一個useEffect代表生命周期中的componentDidUpdate,
useEffect(() => {
document.title = count;
}); // 不用第二個參數(shù)的情況執(zhí)行幾次呢((不傳數(shù)組意味著每一次執(zhí)行都會))
// (只調(diào)用一次) 第二個useEffect代表生命周期中的componentDidMount組件更新 和 componentWillMount組件卸載
useEffect(() => {
window.addEventListener("resize", onResize, false);
return () => {
window.removeEventListener("resize", onResize, false);
};
}, []); //第二個參數(shù)才是useEffect精髓,并且能優(yōu)化性能,只有數(shù)組的每一項都不變的情況下,useEffect才不會執(zhí)行
useEffect(() => {
console.log("count:", count);
}, [count]); //第二個參數(shù)我們傳入 [count], 表示只有 count 的變化時,我才打印 count 值,resize 變化不會打印。
return (
<button
type="button"
onClick={() => {
setCount(count + 1);
}}
>
Click({count}) size: {size.width}x{size.height}
</button>
);
}
export default UseEffectHook;
對于上述代碼的第一個 useEffect,相比類組件,Hooks 不在關(guān)心是 mount 還是 update。用 useEffect統(tǒng)一在渲染后調(diào)用,就完整追蹤了 count 的值。
對于第二個 useEffect,我們可以通過返回一個回調(diào)函數(shù)來注銷事件的注冊。回調(diào)函數(shù)在視圖被銷毀之前觸發(fā),銷毀的原因有兩種:「重新渲染和組件卸載」。
這邊有個問題,既然 useEffect 每次渲染后都執(zhí)行,難道我們每次都要綁定和解綁事件嗎?當(dāng)然是完全不需要,只要使用 useEffect 第二個參數(shù),并傳入一個空數(shù)組即可。第二個參數(shù)是一個可選的數(shù)組參數(shù),只有數(shù)組的每一項都不變的情況下,useEffect 才不會執(zhí)行。第一次渲染之后,useEffect 肯定會執(zhí)行。由于我們傳入的空數(shù)組,空數(shù)組與空數(shù)組是相同的,因此 useEffect 只會在第一次執(zhí)行一次。
這也說明我們把 resize 相關(guān)的邏輯放在一起寫,不在像類組件那樣分散在兩個不同的生命周期內(nèi)。同時我們處理 title 的邏輯與 resize 的邏輯分別在兩個 useEffect 內(nèi)處理,實現(xiàn)關(guān)注點分離,不同的事情要分開放。
以上驗證了hooks組件相對于類組件編寫副作用邏輯類型有兩點:
「1.提高了代碼復(fù)用」
「2.實現(xiàn)了關(guān)注點分離」
我們在定義一個 useEffect,來看看通過不同參數(shù),第二個參數(shù)的不同作用。
useEffect(() => {
console.log('count:', count)
}, [count])//第二個參數(shù)我們傳入 [count], 表示只有 count 的變化時,我才打印 count 值,resize 變化不會打印。

第二個參數(shù)的三種形態(tài),undefined,空數(shù)組及非空數(shù)組,我們都經(jīng)歷過了,但是咱們沒有看到過回調(diào)函數(shù)的執(zhí)行。
我現(xiàn)在描述一種場景就是在組件中訪問 Dom 元素,在 Dom元素上綁定事件,在上述的代碼中添加以下代碼:
...
const onClick = () => {
console.log('click');
}
// 新增一個 DOM 元素,在新的 useEffect 中監(jiān)聽 span 元素的點擊事件。
// useEffect(() => {
// document.querySelector('#size').addEventListener('click', onClick, // // // false);
// },[])
useEffect(() => {
document.querySelector("#size").addEventListener("click", onClick, false);
return () => {
document
.querySelector("#size")
.removeEventListener("click", onClick, false);
};
}, []);
return (
<div>
...
{/* 情況一::*/}
{/* <span id="size">size: {size.width}x{size.height}</span>*/}
{/* 情況二:假如我們 span 元素可以被銷毀重建,我們看看會發(fā)生什么情況,改造一下代碼:*/}
{
count%2
? <span id="size">我是span</span>
: <p id="size">我是p</p>
}
</div>
)
情況一打印結(jié)果:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useEffectHook.js

情況二打印結(jié)果:

可以看出一旦 dom 元素被替換,我們綁定的事件就失效了,所以咱們始終要追蹤這個dom 元素的最新狀態(tài)。
使用 useEffect ,最合適的方式就是使用回調(diào)函數(shù)來處理了,同時要保證每次渲染后都要重新運行,所以不能給第二次參數(shù)設(shè)置 [],改造如下:
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
return () => {
document.querySelector('#size').removeEventListener('click', onClick, false);
}
})
情況三:

無論是之前的生命周期函數(shù),還是useEffect都是處理副作用的,之前的生命周期函數(shù)在命名的時候比較容易理解,但其實都是圍繞著組件的渲染和重渲染的;useEffect抽象了一層,通過第二個參數(shù)執(zhí)行的時機與生命周期是等價的,大家需要理解什么樣的useEffect參數(shù)與什么樣的生命周期函數(shù)是對應(yīng)的,差不多也就靈活運用useEffect了。當(dāng)然參數(shù)化的useEffect肯定不止有這點能耐,只要你能精確控制第二個參數(shù),就能節(jié)省運行時性能還能寫出可維護性很高的代碼,無論如何還是建議多練習(xí)。
問題:useEffect的第二個參數(shù)只要在數(shù)組成員都不變的情況下才不會運行副作用,那么如何理解這個不變呢?
3.使用 Context Hooks
我們已經(jīng)學(xué)了useState和useEffect,他倆已經(jīng)能滿足大部分的開發(fā)需求了,但是為了開發(fā)效率和性能著想,我們接下來還要認(rèn)識使用率沒那么高,但是在函數(shù)組件的編寫中依然發(fā)揮著重要角色的幾個hook函數(shù)
useContext
這個context就是React高級用法中提到的Context,復(fù)習(xí)一下Context,Context能夠允許數(shù)據(jù)跨越組件層級直接傳遞。
使用 Context ,首先頂層先聲明 Provier 組件,并聲明 value 屬性,接著在后代組件中聲明 Consumer組件,這個 Consumer 子組件,只能是唯一的一個函數(shù),函數(shù)參數(shù)即是 Context 的負(fù)載。如果有多個 Context , Provider 和 Consumer 任意的順序嵌套即可。

此外我們還可以針對任意一個 Context 使用 contextType 來簡化對這個 Context 負(fù)載的獲取。但在一個組件中,即使消費多個 Context, contextType 也只能指向其中一個。

在 Hooks 環(huán)境中,依舊可以使用 Consumer,但是 ContextType 作為類靜態(tài)成員肯定是用不了。Hooks 提供了 useContext,不但解決了 Consumer 難用的問題同時也解決了 contextType 只能使用一個 context 的問題。

最基本的Context使用栗子:6.Context Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/contextDemo.js
import React, { Component, createContext, useState,useContext } from "react";
const CountContext = createContext();
// 1.使用類形式的栗子:
class Foo extends Component {
render() {
return (
<CountContext.Consumer>
{(count) => <h1>基本 Context Demo:{count}</h1>}
</CountContext.Consumer>
);
}
}
// 2.接著將 Foo 改成用 contextType 的形式:
class Bar extends Component {
static contextType = CountContext;
render() {
const count = this.context;
return <h1>使用contextType Context Demo:{count}</h1>;
}
}
// 3.接著使用 useContext 形式:
// 在函數(shù)組件中,useContext可不是緊緊可以獲取一個Context,從語法上看對Context的數(shù)量完全沒有限制;解決了 Consumer 難用的問題同時也解決了 contextType 只能使用一個 context 的問題。
function Counter() {
const count = useContext(CountContext);
return <h1>在函數(shù)組件中,useContext demo:{count}</h1>;
}
function ContextDemo(props) {
const [count, setCount] = useState(0);
return (
<div>
<button
type="button"
onClick={() => {
setCount(count + 1);
}}
>
點擊({count})
</button>
<CountContext.Provider value={count}>
<Foo />
<Bar />
<Counter/>
</CountContext.Provider>
</div>
);
}
export default ContextDemo;
useContext 接收一個 context 對象(React.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <CountContext.Provider> 的 value prop 決定。
當(dāng)組件上層最近的 更新時,該 Hook 會觸發(fā)重渲染,并使用最新傳遞給 CountContext provider 的 context value 值。
別忘記 useContext 的參數(shù)必須是 context 對象本身:
「正確:useContext(MyContext)」 錯誤:useContext(MyContext.Consumer) 錯誤:useContext(MyContext.Provider) 調(diào)用了 useContext 的組件總會在 context 值變化時重新渲染。如果重渲染組件的開銷較大,你可以 通過使用 memoization 來優(yōu)化。
4.使用 Memo /CallBack Hooks
useCallback是useMemo的變種。
回顧:meno 用來優(yōu)化函數(shù)組件重渲染的行為,當(dāng)傳入屬性值都不變的情況下,就不會觸發(fā)組件的重渲染,否則就會觸發(fā)組件重渲染。



useMemo 與 memo

memo針對的是一個組件的渲染是否重復(fù)執(zhí)行,而 useMemo 定義的是一段函數(shù)邏輯是否重復(fù)執(zhí)行,本質(zhì)都是利用同樣的算法來判定依賴是否發(fā)生改變,進而決定是否觸發(fā)特定邏輯。有很多這樣的邏輯輸入輸出是對等的,相同的輸入一定產(chǎn)生相同的輸出,數(shù)學(xué)上稱之為冪等,useMemo就可以減少重復(fù)的重復(fù)計算減少資源浪費。所以嚴(yán)格來講不適用memo或者useMemo不應(yīng)該會導(dǎo)致你的業(yè)務(wù)邏輯發(fā)生變化,換句話說memo和useMemo僅僅用來做性能優(yōu)化使用。
memo和useMemo僅僅用來性能優(yōu)化使用。
舉個栗子:7.useMemo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook.js
import React, { useState, useMemo } from "react";
function Foo(props) {
return <h1>{props.count}</h1>;
}
function UseMemoHook(props) {
const [count, setCount] = useState(0);
const double = useMemo(() => {
return count * 2;
}, [count]);
return (
<div>
<button
type="button"
onClick={() => {
setCount(count + 1);
}}
>
Click ({count}) double : ({double})
</button>
<Foo count={count} />
</div>
);
}
export default UseMemoHook;
如上所示, useMemo 語法與 useEffect 是一致的。第一個參數(shù)是需要執(zhí)行的邏輯函數(shù),第二個參數(shù)是這個邏輯依賴輸入變量組成的數(shù)組,如果不傳第二個參數(shù),這 useMemo 的邏輯每次就會運行, useMemo 本身的意義就不存在了,所以需要傳入?yún)?shù)。所以傳入空數(shù)組就只會運行一次,策略與 useEffect 是一樣的,但有一點比較大的差異就是調(diào)用時機, useEffect 執(zhí)行的是副作用,所以一定是渲染之后才執(zhí)行,但 useMemo 是需要返回值的,而返回值可以直接參與渲染,因此 useMemo 是在渲染期間完成的,有這樣一前一后的區(qū)別。
稍微改動一下,count===3,則參數(shù)就變成了布爾值Boolean
const double = useMemo(() => {
return count * 2;
}, [count === 3]);
https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook.js
現(xiàn)在能斷定, count 在等于 3 之前,由于這個條件一直保存 false 不變,double 不會重新計算,所以一直是 0,當(dāng) count 等于 3, double 重新計算為 6,當(dāng) count 大于 3, double 在重新計算,變成 8,然后就一直保存 8 不變。所以只要找到你真正依賴了哪些參數(shù),就能盡可能的避免沒有必要的計算。
useMemo也可以依賴另外一個useMemo,比如:
//useMemo也可以依賴另外一個useMemo,比如:
const half = useMemo(()=>{
return double/4
},[double])
但是一定不要「循環(huán)依賴」,會把瀏覽器搞崩潰的
記住,傳入的 useMemo 的函數(shù)會在渲染期間執(zhí)行,請不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的聽任,諸如副作用這類操作屬于 useEffect 的適用范疇,而不是 useMemo。
「你可以把 useMemo 作為性能優(yōu)化的手段,但不要把它當(dāng)成語義上的保證?!?/strong>
使用Callback Hooks
先重現(xiàn)一個使用 memo 優(yōu)化子組件重渲染的場景:8.memo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook2.js
import React, { useState, useMemo, memo } from "react";
const Counter = memo(function Counter(props) {
console.log("counter");
return <h1>{props.count}</h1>;
});
function UseMemoHook(props) {
const [count, setCount] = useState(0);
//稍微改動一下,count===3,則參數(shù)就變成了布爾值Boolean
const double = useMemo(() => {
return count * 2;
}, [count === 3]);
//useMemo也可以依賴另外一個useMemo,比如:
const half = useMemo(() => {
return double / 4;
}, [double]);
return (
<div>
<button
type="button"
onClick={() => {
setCount(count + 1);
}}
>
Click ({count}) double : ({double}) half:({half})
</button>
<Counter count={double} />
</div>
);
}
export default UseMemoHook;
使用 memo 包裹 Counter 組件,這樣只有當(dāng) double 變化時, Counter 組件才會重新渲染,執(zhí)行里面的 log,運行結(jié)果請看8.memo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook2.js
現(xiàn)在在給 Counter 中的 h1 添加一個 click 事件:
const Counter = memo(function Counter(props) {
console.log("counter");
return <h1 onClick={props.onClick}>{props.count}</h1>;
});
然后在 UseMemoHook2 組件中聲明 onClick 并傳給 Counter組件:
function UseMemoHook2(props) {
...
// 優(yōu)化前
const onClick=()=>{
console.log('onClick')
}
return (
<div>
...
<Counter count={double} onClick={onClick} />
</div>
);
}
export default UseMemoHook2;
結(jié)果請看8.memo Demo 優(yōu)化前片段

可以看出,每次點擊,不管 Counter 是否有變化, Counter 組件都會被渲染。那就說明每次UseMemoHook2 重新渲染之后, onClick 句柄的變化,導(dǎo)致 Counter 也被連帶重新渲染了。count 經(jīng)常變化可以理解,但是 onClick就不應(yīng)該經(jīng)常變化了,畢竟只是一個函數(shù)而已,所以我們要想辦法讓 onClick 句柄不變化。
想想我們上面講的 useMemo,可以這樣來優(yōu)化 onClick:
// 優(yōu)化后
const onClick = useMemo(() => {
return ()=>{
console.log("onClick")
}
},[])
由于我們傳給 useMemo 的第二個參數(shù)是一個空數(shù)組,那么整個邏輯就只會運行一次,理論上我們返回的 onClick 就只有一個句柄。
運行效果:請看8.memo Demo 優(yōu)化后片段
現(xiàn)在我們把 useCallback 來實現(xiàn)上頁 useMemo 的邏輯。
// 優(yōu)化后
const onClick = useCallback(() => {
console.log("onClick")
},[])
如果 useMemo 返回的是一個函數(shù),那么可以直接使用 useCallback 來省略頂層的函數(shù)。
大家可能有一個疑問, useCallback 這幾行代碼明明每次組件渲染都會創(chuàng)建新的函數(shù),它怎么就優(yōu)化性能了呢?
注意,大家不要誤會,使用 useCallback 確實不能阻止創(chuàng)建新的函數(shù),但這個函數(shù)不一定會被返回,換句話說很可能這個新創(chuàng)建的函數(shù)就被拋棄不用了。useCallback解決的是:傳入子組件的函數(shù)參數(shù)過度變化,導(dǎo)致子組件過度渲染的問題,這里一定要理解好,不要對useCallback有誤解。
上述我們這個onClick函數(shù)什么都不依賴,因此useCallback的第二個參數(shù)才是空數(shù)組,也就是讓useCallback的邏輯只運行一次,在實際業(yè)務(wù)中不會這么簡單,至少也要更新一下狀態(tài),舉個栗子:
function useMemo(props) {
...
const [clickCount, setClickCount] = useState(0);
const onClick = useCallback(() => {
console.log("Click");
setClickCount(clickCount + 1);
}, [clickCount, setClickCount]);//setClickCount不需要寫,因為 React 能保證 setState 每次返回的都是同個句柄
...
}
在 UseMemoHook2 組件中在聲明一個 useState,然后在 onClick 中調(diào)用 setClickCount,此時 onClick 依賴 clickCount, setClickCount。
其實這里的 setClickCount 是不需要寫的,因為 React 能保證 setState 每次返回的都是同個句柄。不信,可以看下官方文檔 :
?注意
React 會確保
?dispatch函數(shù)的標(biāo)識是穩(wěn)定的,并且不會在組件重新渲染時改變。這就是為什么可以安全地從useEffect或useCallback的依賴列表中省略dispatch。
這里的場景,除了直接使用 setClickCount+1 賦值以外, 還有一種方式甚至連 clickCount都不用依賴。setState 除了傳入對應(yīng)的 state 最新值以外,還可以傳入一個函數(shù),函數(shù)的參數(shù)即這個 state的當(dāng)前值,返回就是要更新的值:
// 傳入一個函數(shù),函數(shù)的參數(shù)即這個 `state`的當(dāng)前值,返回就是要更新的值
const onClick = useCallback(() => {
console.log("Click");
setClickCount((clickCount) => clickCount + 1);
}, []);
useMemo、useCallback小結(jié)
和 memo 根據(jù)屬性來決定是否重新渲染組件一樣, useMemo 可以根據(jù)指定的依賴來決定一段函數(shù)邏輯是否重新執(zhí)行,從而優(yōu)化性能。
如果 useMemo 的返回值是函數(shù)的話,那么就可以簡寫成 useCallback 的方式,只是簡寫而已,實際并沒有區(qū)別。
需要特別注意的是,當(dāng)依賴變化時,我們能斷定 useMemo 一定重新執(zhí)行。但是注意,即使依賴不變化我們不能假定它就一定不會重新執(zhí)行,也就是說,它也可能重新執(zhí)行,就是考慮內(nèi)存優(yōu)化的結(jié)果??傊瑄seMemo和useCallback是用來錦上添花的優(yōu)化手段,不可以過度依賴他是否觸發(fā)重新渲染,因為React沒有給我們打包票說一定重新執(zhí)行或者一定不重新執(zhí)行,useMemo使用場景很多,特別是useCallback傳遞給useCallback子組件
我們可以把 useMemo, useCallback 當(dāng)做一個錦上添花優(yōu)化手段,不可以過度依賴它是否重新渲染,因為 React 目前沒有打包票說一定執(zhí)行或者一定不執(zhí)行。
5.使用 Ref Hooks

類組件中使用 Ref 一般有:
String Ref Callback Ref CreateRef
上述在函數(shù)組件中沒有辦法使用它們,取而代之的是 useRef Hooks。
useRef 主要有兩個使用場景:
獲取子組件或者 DOM 節(jié)點的句柄 渲染周期之間的共享數(shù)據(jù)的存儲
大家可能會想到 state 也可跨越渲染周期保存,但是 state 的賦值會觸發(fā)重渲染,但是 ref 不會,從這點看 ref 更像是類屬性中的普通成員。
「舉例說明一下:使用useRef獲取子組件或者 DOM 節(jié)點的句柄」:9.useRef Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useRefHook.js
import React, {
PureComponent,
useState,
useMemo,
useCallback,
useRef
} from "react";
// const Counter = memo(function Counter(props) {
// console.log("counter");
// return <h1 onClick={props.onClick}>{props.count}</h1>;
// });
class Counter extends PureComponent {
speak() {
console.log(`now counter is:${this.props.count}`);
}
render() {
const { props } = this;
return <h1 onClick={props.onClick}>{props.count}</h1>;
}
}
function UseRefHook(props) {
const [count, setCount] = useState(0);
// 依賴多個數(shù)據(jù)變化的時候
const [clickCount, setClickCount] = useState(0);
const counterRef = useRef();
//稍微改動一下,count===3,則參數(shù)就變成了布爾值Boolean
const double = useMemo(() => {
return count * 2;
}, [count === 3]);
const onClick = useCallback(() => {
console.log("Click");
setClickCount((clickCount) => clickCount + 1);
console.log(counterRef.current);
counterRef.current.speak();
}, [counterRef]);
return (
<div>
<button
type="button"
onClick={() => {
setCount(count + 1);
}}
>
Click ({count}) double : ({double})
</button>
<Counter ref={counterRef} count={double} onClick={onClick} />
</div>
);
}
export default UseRefHook;
下面我們來嘗試一下,不用ref來保存DOM或者組件,而是保存一個普通變量:
「粟例說明一下:同步渲染周期之間的共享數(shù)據(jù)的存儲」:
假設(shè)我有這樣一個場景,組件一掛載就讓count狀態(tài)每秒鐘自動+1,當(dāng)count>=10以后就不再自動增加,顯然這是一個副作用,首先我們需要引入useEffect,在UseRefHook中我們需要定義兩個副作用:第一個僅作用一次,用來啟動定時器;第二個始終作用用來判斷count的值是否滿足>=10的條件;
let it;
// 第一個ueeEffect:僅作用一次
useEffect(() => {
it = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
// 第二個ueeEffect始終作用用來判斷count的值是否滿足>=10的條件
useEffect(() => {
if (count >= 10) {
clearInterval(it);
}
});

然而結(jié)果到10以后并沒有停止增加,為什么?顯然就是因為在clearInterval的時候,定時器的句柄it這個變量已經(jīng)不是setInterval的賦值了,每次UseRefHook重渲染都會重置他,那我們把it放在state中么?用useState聲明能解決么?但是it并沒有參與渲染,而且弄不好在副作用里面更新或?qū)е滤姥h(huán),那么現(xiàn)在useRef就派上用場了:
let it = useRef();
useEffect(() => {
it.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
useEffect(() => {
if (count >= 10) {
clearInterval(it.current);
}
});
使用 useRef 來創(chuàng)建一個 it, 當(dāng) setInterval 返回的結(jié)果賦值給 it 的 current 屬性。

這回效果到10停止了更新,ref幫我們解決了這個問題,是不是很像類屬性成員,如果遇見
需要訪問上一次渲染時候的數(shù)據(jù),甚至是state,就把他們同步到ref中,下一次渲染就能夠正確的獲取到了。
兩種ref的使用場景都講過了:
useRef 主要有兩個使用場景:
獲取子組件或者 DOM 節(jié)點的句柄 渲染周期之間的共享數(shù)據(jù)的存儲
你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 對象以 <divref={myRef}/> 形式傳入組件,則無論該節(jié)點如何改變,React 都會將 ref 對象的 .current 屬性設(shè)置為相應(yīng)的 DOM 節(jié)點。
然而, useRef() 比 ref 屬性更有用**。它可以很方便地保存任何可變值**,其類似于在 class 中使用實例字段的方式。
這是因為它創(chuàng)建的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current:...} 對象的唯一區(qū)別是, useRef 會在每次渲染時返回同一個 ref 對象。
?請記住,當(dāng) ref 對象內(nèi)容發(fā)生變化時,useRef 并不會通知你。變更 .current 屬性不會引發(fā)組件重新渲染。如果想要在 React 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼,則需要使用回調(diào) ref 來實現(xiàn)。
?
在副作用里面如何判定一個元素或者組件在本次渲染和上次渲染之間有過重新創(chuàng)建呢?
6.自定義 Hooks
前面三篇,我們講到優(yōu)化類組件的三大問題:
方便復(fù)用狀態(tài)邏輯 副作用的關(guān)注點分離 函數(shù)組件無 this問題
對于組件的復(fù)用狀態(tài)沒怎么說明,現(xiàn)在使用自定義 Hook 來說明一下。
首先我們把上面的例子用到 count 的邏輯的用自定義 Hook 封裝起來:10.自定義Hook Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/customHook.js
import React, { useState, useEffect, useRef } from "react";
function useCount(defaultCount) {
const [count, setCount] = useState(0);
let it = useRef();
useEffect(() => {
it.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
useEffect(() => {
if (count >= 10) {
clearInterval(it.current);
}
});
return [count, setCount];
}
function CustomHook(props) {
const [count, setCount] = useCount(0);
return (
<div>
<h1>{count}</h1>
</div>
);
}
export default CustomHook;

可以看出運行效果跟上面是一樣的。
「定義 Hook 是一個函數(shù),其名稱以 “use” 開頭,函數(shù)內(nèi)部可以調(diào)用其他的 Hook」。我們在函數(shù)自定義寫法上似乎和編寫函數(shù)組件沒有區(qū)別,確實自定義組件與函數(shù)組件的最大區(qū)別就是「輸入與輸出的區(qū)別」。
再來一個特別的 Hook 加深一下映像。在上述代碼不變的條件下,我們在加一個自定義 Hook 內(nèi)容如下:
function useCounter(count) {
return <h1>{count}</h1>;
}
function CustomHook(props) {
const [count, setCount] = useCount(0);
const Counter = useCounter(count);
return (
<div>
<h2>{Counter}</h2>
</div>
);
}
我們自定義 useCounter Hook返回的是一個 JSX,運行效果是一樣的,所以 Hook 是可以返回 JSX 來參與渲染的,更說明 Hook 與函數(shù)組件的相似性。
7.Hooks使用法則
只在最頂層使用 Hook
「不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook,」 確??偸窃谀愕?React 函數(shù)的最頂層以及任何 return 之前調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的 useState 和 useEffect 調(diào)用之間保持 hook 狀態(tài)的正確。(如果你對此感到好奇,我們在下面會有更深入的解釋。)
只在 React 函數(shù)中調(diào)用 Hook
**不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook。**你可以:
? 在 React 的函數(shù)組件中調(diào)用 Hook ? 在自定義 Hook 中調(diào)用其他 Hook (我們將會在下一頁 中學(xué)習(xí)這個。)
遵循此規(guī)則,確保組件的狀態(tài)邏輯在代碼中清晰可見。
8.Hooks 使用過程中注意常見問題
官方文章上說的很明白,此處不在贅述:https://zh-hans.reactjs.org/docs/hooks-faq.html
小結(jié)
本文主要是對React Hooks的基本使用的介紹,其中的DEMO鏈接如下:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/index.js
所有demo都被引用在了一個index.js文件中,可根據(jù)文章介紹demo序號進行操作演示

查看本文不方便也可看掘金上已同步:https://juejin.cn/post/7008157923532767246/
參考
慕課《React勁爆新特性Hooks 重構(gòu)旅游電商網(wǎng)站火車票PWA》 react 官網(wǎng)
