React Hooks 基本使用詳解
Hooks let you use state and other React features without writing a class
Hooks 可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性
「一、類組件不足:」
-
Render Props
-
HOC(調用時候比 render props 方便)
-
-
-
-
-
缺少複用機制
-
渲染屬性 Render Props 和高階組件 HOC 導致層級冗餘
-
生命週期函數經常包含不相干邏輯
-
相干邏輯被打散在不同生命週期, 理解代碼邏輯也很喫力
-
this 指向困擾
-
內聯函數過度創建新句柄(每次都是新的,會重新觸發,導致子組件不停渲染)
-
類成員函數不能保證 this
二、「Hooks 優勢(優化類組件的三大問題):」
- 副作用的關注點分離(不是發生在數據向視圖轉化之中,都是在之外的。例如:發起網絡請求、訪問原型上的 DOM 元素、寫本地持久化緩存、綁定解綁事件都是數據渲染視圖之外的。這些一般都是放在生命週期中的。useEffect 都是在每次渲染完成之後調用)
研發挑戰:
一開始的時候覺得 hooks 非常地神祕,寫慣了 class 式的組件後,我們的思維就會定格在那裏,生命週期,state,this 等的使用。因此會以 class 編寫的模式去寫函數式組件,導致我們一次又一次地爬坑,接下來我們就開始我們的實現方式講解。(提示:以下是都只是一種簡單的模擬方法,與實際有一些差別,但是核心思想是一致的)
三、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
來實現上述功能。
栗子:1.useState Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useStateHook.js
import React, {useState} from 'react'
function App () {
//返回兩個參數 一個是當前的狀態,還有一個是修改當前狀態的函數
const [count, setCount] = useState(0)
// 動態傳入參數 //延遲初始化,提高效率,只會在初始化時候執行一次
// 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。通過在函數組件裏調用它來給組件添加一些內部 state
,React 會在重複渲染時保留這個 state
。
useState
會返回一對值 **:當前狀態 ** 和一個讓你更新它的函數。你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class
組件的 this.setState
,但是它不會把新的 state
和舊的 state
進行合併。useState
唯一的參數就是初始 state
。
useState
讓代碼看起來簡潔了,但是我們可能會對組件中,直接調用 useState
返回的狀態會有些懵。既然 useState
沒有傳入任何的環境參數,它怎麼知道要返回的的是 count
的呢?而且還是這個組件的 count
不是其它組件的 count
?
初淺的理解: useState
確實不知道我們要返回的 count
,但其實也不需要知道,它只要返回一個變量就行了。**「數組解構」**的語法讓我們在調用 useState
時可以給 state
變量取不同的名字。
useState
怎麼知道要返回當前組件的 state
?
因爲 JavaScript 是單線程的。在 useState
被調用時,它只能在唯一一個組件的上下文中。
有人可能會問,如果一個組件內有多個 usreState
,那 useState
怎麼知道哪一次調用返回哪一個 state
呢?
這個就是按照第一次運行的次序來順序來返回的。
接着上面的例子我們在聲明一個 useState
:
...
const [count, setScount] = useState(0)
const [name, setName] = useState('小圓臉兒')
...
爲了防止我們使用 useState 不當,React 提供了一個 ESlint 插件幫助我們檢查。
- eslint-plugin-react-hooks
「優化點」
通過上述我們知道 useState
有個默認值,因爲是默認值,所以在不同的渲染週期去傳入不同的值是沒有意義的,只有第一次傳入的纔有效。如下所示:
...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...
state
的默認值是基於 props
, 在 APP 組件每次渲染的時候 const defaultCount = props.defaultCount || 0
都會運行一次,如果它複雜度比較高的話,那麼浪費的資料肯定是可觀的。
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
2. 使用 Effect Hooks
副作用的時機
-
Mount 之後 對應
componentDidMount
-
Update 之後 對應
componentDidUpdate
-
Unmount 之前 對應
componentWillUnmount
現在使用 useEffect
就可以覆蓋上述的情況。
爲什麼一個 useEffect
就能涵蓋 Mount,Update,Unmount
等場景呢?
useEffect 標準上是在組件每次渲染之後調用,並且會根據自定義狀態來決定是否調用還是不調用。
第一次調用就相當於componentDidMount
,後面的調用相當於 componentDidUpdate
。useEffect
還可以返回另一個回調函數,這個函數的執行時機很重要。作用是清除上一次副作用遺留下來的狀態。
比如一個組件在第三次,第五次,第七次渲染後執行了 useEffect
邏輯,那麼回調函數就會在第四次,第六次和第八次渲染之前執行。嚴格來講,是在前一次的渲染視圖清除之前。如果 useEffect
是在第一次調用的,那麼它返回的回調函數就只會在組件卸載之前調用了,也就是componentWillUnmount
。
如果你熟悉 React class 的生命週期函數,你可以把 useEffect Hook 看做
componentDidMount
,componentDidUpdate
和componentWillUnmount
這三個函數的組合。
在線舉栗子說明一下: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;
對於生命週期我們在複習一遍:
上面主要做的就是網頁 title
顯示count
值,並監聽網頁大小的變化。這裏用到了componentDidMount
,componentDidUpdate
等副作用,因爲第一次掛載我們需要把初始值給 title, 當 count
變化時,把變化後的值給它 title
,這樣 title
才能實時的更新。
「注意,我們需要在兩個生命週期函數中編寫重複的代碼。」
這邊我們容易出錯的地方就是在組件結束之後要**「記住銷燬事件的註冊」**,不然會導致資源的泄漏。現在我們把 App 組件的副作用用 useEffect
實現。
在線栗子 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;
}); // 不用第二個參數的情況執行幾次呢((不傳數組意味着每一次執行都會))
// (只調用一次) 第二個useEffect代表生命週期中的componentDidMount組件更新 和 componentWillMount組件卸載
useEffect(() => {
window.addEventListener("resize", onResize, false);
return () => {
window.removeEventListener("resize", onResize, false);
};
}, []); //第二個參數纔是useEffect精髓,並且能優化性能,只有數組的每一項都不變的情況下,useEffect纔不會執行
useEffect(() => {
console.log("count:", count);
}, [count]); //第二個參數我們傳入 [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 不在關心是 mount 還是 update。用 useEffect
統一在渲染後調用,就完整追蹤了 count
的值。
對於第二個 useEffect
, 我們可以通過返回一個回調函數來註銷事件的註冊。回調函數在視圖被銷燬之前觸發,銷燬的原因有兩種:「重新渲染和組件卸載」。
這邊有個問題,既然 useEffect
每次渲染後都執行,難道我們每次都要綁定和解綁事件嗎?當然是完全不需要,只要使用 useEffect
第二個參數,並傳入一個空數組即可。第二個參數是一個可選的數組參數,只有數組的每一項都不變的情況下,useEffect
纔不會執行。第一次渲染之後, useEffect 肯定會執行。由於我們傳入的空數組,空數組與空數組是相同的,因此 useEffect
只會在第一次執行一次。
這也說明我們把 resize
相關的邏輯放在一起寫,不在像類組件那樣分散在兩個不同的生命週期內。同時我們處理 title 的邏輯與 resize 的邏輯分別在兩個 useEffect 內處理,實現關注點分離,不同的事情要分開放。
以上驗證了 hooks 組件相對於類組件編寫副作用邏輯類型有兩點:
「1. 提高了代碼複用」
「2. 實現了關注點分離」
useEffect(() => {
console.log('count:', count)
}, [count])//第二個參數我們傳入 [count], 表示只有 count 的變化時,我纔打印 count 值,resize 變化不會打印。
第二個參數的三種形態,undefined
, 空數組及非空數組,我們都經歷過了,但是咱們沒有看到過回調函數的執行。
我現在描述一種場景就是在組件中訪問 Dom 元素,在 Dom 元素上綁定事件,在上述的代碼中添加以下代碼:
...
const onClick = () => {
console.log('click');
}
// 新增一個 DOM 元素,在新的 useEffect 中監聽 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>size: {size.width}x{size.height}</span>*/}
{/* 情況二:假如我們 span 元素可以被銷燬重建,我們看看會發生什麼情況,改造一下代碼:*/}
{
count%2
? <span>我是span</span>
: <p>我是p</p>
}
</div>
)
情況一打印結果:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useEffectHook.js
情況二打印結果:
可以看出一旦 dom 元素被替換,我們綁定的事件就失效了,所以咱們始終要追蹤這個 dom 元素的最新狀態。
使用 useEffect
,最合適的方式就是使用回調函數來處理了,同時要保證每次渲染後都要重新運行,所以不能給第二次參數設置 []
,改造如下:
useEffect(() => {
document.querySelector('#size').addEventListener('click', onClick, false);
return () => {
document.querySelector('#size').removeEventListener('click', onClick, false);
}
})
情況三:
無論是之前的生命週期函數,還是 useEffect 都是處理副作用的,之前的生命週期函數在命名的時候比較容易理解,但其實都是圍繞着組件的渲染和重渲染的;useEffect 抽象了一層,通過第二個參數執行的時機與生命週期是等價的,大家需要理解什麼樣的 useEffect 參數與什麼樣的生命週期函數是對應的,差不多也就靈活運用 useEffect 了。當然參數化的 useEffect 肯定不止有這點能耐,只要你能精確控制第二個參數,就能節省運行時性能還能寫出可維護性很高的代碼,無論如何還是建議多練習。
問題:useEffect 的第二個參數只要在數組成員都不變的情況下才不會運行副作用,那麼如何理解這個不變呢?
3. 使用 Context Hooks
我們已經學了 useState 和 useEffect,他倆已經能滿足大部分的開發需求了,但是爲了開發效率和性能着想,我們接下來還要認識使用率沒那麼高,但是在函數組件的編寫中依然發揮着重要角色的幾個 hook 函數
useContext
這個 context 就是 React 高級用法中提到的 Context,複習一下 Context,Context 能夠允許數據跨越組件層級直接傳遞。
使用 Context , 首先頂層先聲明 Provier 組件,並聲明 value 屬性,接着在後代組件中聲明 Consumer 組件,這個 Consumer 子組件,只能是唯一的一個函數,函數參數即是 Context 的負載。如果有多個 Context , Provider 和 Consumer 任意的順序嵌套即可。
此外我們還可以針對任意一個 Context
使用 contextType
來簡化對這個 Context
負載的獲取。但在一個組件中,即使消費多個 Context
, contextType
也只能指向其中一個。
在 Hooks 環境中,依舊可以使用 Consumer
,但是 ContextType
作爲類靜態成員肯定是用不了。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 形式:
// 在函數組件中,useContext可不是緊緊可以獲取一個Context,從語法上看對Context的數量完全沒有限制;解決了 Consumer 難用的問題同時也解決了 contextType 只能使用一個 context 的問題。
function Counter() {
const count = useContext(CountContext);
return <h1>在函數組件中,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 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <CountContext.Provider> 的 value prop 決定。
當組件上層最近的 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 CountContext provider 的 context value 值。
別忘記 useContext 的參數必須是 context 對象本身:
-
「正確:useContext(MyContext)」
-
錯誤:useContext(MyContext.Consumer)
-
錯誤:useContext(MyContext.Provider) 調用了 useContext 的組件總會在 context 值變化時重新渲染。如果重渲染組件的開銷較大,你可以 通過使用 memoization 來優化。
4. 使用 Memo /CallBack Hooks
useCallback 是 useMemo 的變種。
回顧:meno 用來優化函數組件重渲染的行爲,當傳入屬性值都不變的情況下,就不會觸發組件的重渲染,否則就會觸發組件重渲染。
useMemo 與 memo
image-20210913013342998
memo
針對的是一個組件的渲染是否重複執行,而 useMemo
定義的是一段函數邏輯是否重複執行,本質都是利用同樣的算法來判定依賴是否發生改變,進而決定是否觸發特定邏輯。有很多這樣的邏輯輸入輸出是對等的,相同的輸入一定產生相同的輸出,數學上稱之爲冪等,useMemo 就可以減少重複的重複計算減少資源浪費。所以嚴格來講不適用 memo 或者 useMemo 不應該會導致你的業務邏輯發生變化,換句話說 memo 和 useMemo 僅僅用來做性能優化使用。
memo 和 useMemo 僅僅用來性能優化使用。
舉個栗子: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 是一致的。第一個參數是需要執行的邏輯函數,第二個參數是這個邏輯依賴輸入變量組成的數組,如果不傳第二個參數,這 useMemo 的邏輯每次就會運行, useMemo 本身的意義就不存在了,所以需要傳入參數。所以傳入空數組就只會運行一次,策略與 useEffect 是一樣的,但有一點比較大的差異就是調用時機, useEffect 執行的是副作用,所以一定是渲染之後才執行,但 useMemo 是需要返回值的,而返回值可以直接參與渲染,因此 useMemo 是在渲染期間完成的, 有這樣一前一後的區別。
稍微改動一下,count===3,則參數就變成了布爾值 Boolean
const double = useMemo(() => {
return count * 2;
}, [count === 3]);
https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook.js
現在能斷定, count
在等於 3 之前,由於這個條件一直保存 false
不變,double 不會重新計算,所以一直是 0,當 count
等於 3, double
重新計算爲 6,當 count
大於 3, double
在重新計算,變成 8,然後就一直保存 8 不變。所以只要找到你真正依賴了哪些參數,就能儘可能的避免沒有必要的計算。
useMemo 也可以依賴另外一個 useMemo,比如:
//useMemo也可以依賴另外一個useMemo,比如:
const half = useMemo(()=>{
return double/4
},[double])
但是一定不要**「循環依賴」**,會把瀏覽器搞崩潰的
記住, 傳入的 useMemo
的函數會在渲染期間執行,請不要在這個函數內部執行與渲染無關的聽任,諸如副作用這類操作屬於 useEffect
的適用範疇,而不是 useMemo
。
「你可以把 useMemo 作爲性能優化的手段,但不要把它當成語義上的保證。」
使用 Callback Hooks
先重現一個使用 memo
優化子組件重渲染的場景: 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,則參數就變成了布爾值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
組件,這樣只有當 double
變化時, Counter
組件纔會重新渲染,執行裏面的 log, 運行結果請看 8.memo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook2.js
現在在給 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) {
...
// 優化前
const onClick=()=>{
console.log('onClick')
}
return (
<div>
...
<Counter count={double} onClick={onClick} />
</div>
);
}
export default UseMemoHook2;
結果請看 8.memo Demo 優化前片段
可以看出,每次點擊,不管 Counter
是否有變化, Counter
組件都會被渲染。那就說明每次 UseMemoHook2 重新渲染之後, onClick
句柄的變化,導致 Counter
也被連帶重新渲染了。count
經常變化可以理解,但是 onClick
就不應該經常變化了,畢竟只是一個函數而已,所以我們要想辦法讓 onClick
句柄不變化。
想想我們上面講的 useMemo
,可以這樣來優化 onClick
:
// 優化後
const onClick = useMemo(() => {
return ()=>{
console.log("onClick")
}
},[])
由於我們傳給 useMemo
的第二個參數是一個空數組,那麼整個邏輯就只會運行一次,理論上我們返回的 onClick
就只有一個句柄。
運行效果:請看 8.memo Demo 優化後片段
現在我們把 useCallback
來實現上頁 useMemo
的邏輯。
// 優化後
const onClick = useCallback(() => {
console.log("onClick")
},[])
如果 useMemo
返回的是一個函數,那麼可以直接使用 useCallback
來省略頂層的函數。
大家可能有一個疑問, useCallback
這幾行代碼明明每次組件渲染都會創建新的函數,它怎麼就優化性能了呢?
注意,大家不要誤會,使用 useCallback
確實不能阻止創建新的函數,但這個函數不一定會被返回,換句話說很可能這個新創建的函數就被拋棄不用了。useCallback
解決的是:傳入子組件的函數參數過度變化,導致子組件過度渲染的問題,這裏一定要理解好,不要對 useCallback 有誤解。
上述我們這個 onClick 函數什麼都不依賴,因此 useCallback 的第二個參數纔是空數組,也就是讓 useCallback 的邏輯只運行一次,在實際業務中不會這麼簡單,至少也要更新一下狀態,舉個栗子:
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
中調用 setClickCount
,此時 onClick 依賴 clickCount
, setClickCount
。
其實這裏的 setClickCount
是不需要寫的,因爲 React 能保證 setState
每次返回的都是同個句柄。不信,可以看下官方文檔 :
❝
注意
React 會確保
dispatch
函數的標識是穩定的,並且不會在組件重新渲染時改變。這就是爲什麼可以安全地從useEffect
或useCallback
的依賴列表中省略dispatch
。❞
這裏的場景,除了直接使用 setClickCount+1
賦值以外, 還有一種方式甚至連 clickCount
都不用依賴。setState
除了傳入對應的 state
最新值以外,還可以傳入一個函數,函數的參數即這個 state
的當前值,返回就是要更新的值:
// 傳入一個函數,函數的參數即這個 `state`的當前值,返回就是要更新的值
const onClick = useCallback(() => {
console.log("Click");
setClickCount((clickCount) => clickCount + 1);
}, []);
useMemo、useCallback 小結
和 memo
根據屬性來決定是否重新渲染組件一樣, useMemo
可以根據指定的依賴來決定一段函數邏輯是否重新執行,從而優化性能。
如果 useMemo
的返回值是函數的話,那麼就可以簡寫成 useCallback
的方式,只是簡寫而已,實際並沒有區別。
需要特別注意的是,當依賴變化時,我們能斷定 useMemo
一定重新執行。但是注意,即使依賴不變化我們不能假定它就一定不會重新執行,也就是說,它也可能重新執行,就是考慮內存優化的結果。總之,useMemo 和 useCallback 是用來錦上添花的優化手段,不可以過度依賴他是否觸發重新渲染,因爲 React 沒有給我們打包票說一定重新執行或者一定不重新執行,useMemo 使用場景很多,特別是 useCallback 傳遞給 useCallback 子組件
我們可以把 useMemo
, useCallback
當做一個錦上添花優化手段,不可以過度依賴它是否重新渲染,因爲 React 目前沒有打包票說一定執行或者一定不執行。
5. 使用 Ref Hooks
類組件中使用 Ref 一般有:
-
String Ref
-
Callback Ref
-
CreateRef
上述在函數組件中沒有辦法使用它們,取而代之的是 useRef
Hooks。
useRef
主要有兩個使用場景:
-
獲取子組件或者 DOM 節點的句柄
-
渲染週期之間的共享數據的存儲
大家可能會想到 state 也可跨越渲染週期保存,但是 state
的賦值會觸發重渲染,但是 ref
不會,從這點看 ref 更像是類屬性中的普通成員。
「舉例說明一下:使用 useRef 獲取子組件或者 DOM 節點的句柄」: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);
// 依賴多個數據變化的時候
const [clickCount, setClickCount] = useState(0);
const counterRef = useRef();
//稍微改動一下,count===3,則參數就變成了布爾值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 或者組件,而是保存一個普通變量:
「粟例說明一下:同步渲染週期之間的共享數據的存儲」:
假設我有這樣一個場景,組件一掛載就讓 count 狀態每秒鐘自動 + 1,當 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);
}
});
然而結果到 10 以後並沒有停止增加,爲什麼?顯然就是因爲在 clearInterval 的時候,定時器的句柄 it 這個變量已經不是 setInterval 的賦值了,每次 UseRefHook 重渲染都會重置他,那我們把 it 放在 state 中麼?用 useState 聲明能解決麼?但是 it 並沒有參與渲染,而且弄不好在副作用裏面更新或導致死循環,那麼現在 useRef 就派上用場了:
let it = useRef();
useEffect(() => {
it.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []);
useEffect(() => {
if (count >= 10) {
clearInterval(it.current);
}
});
使用 useRef
來創建一個 it
, 當 setInterval
返回的結果賦值給 it
的 current
屬性。
這回效果到 10 停止了更新,ref 幫我們解決了這個問題,是不是很像類屬性成員,如果遇見
需要訪問上一次渲染時候的數據,甚至是 state,就把他們同步到 ref 中,下一次渲染就能夠正確的獲取到了。
兩種 ref 的使用場景都講過了:
useRef
主要有兩個使用場景:
-
獲取子組件或者 DOM 節點的句柄
-
渲染週期之間的共享數據的存儲
你應該熟悉 ref
這一種訪問 DOM 的主要方式。如果你將 ref
對象以 <divref={myRef}/>
形式傳入組件,則無論該節點如何改變,React 都會將 ref
對象的 .current
屬性設置爲相應的 DOM 節點。
然而, useRef()
比 ref
屬性更有用 **。它可以很方便地保存任何可變值 **,其類似於在 class 中使用實例字段的方式。
這是因爲它創建的是一個普通 Javascript 對象。而 useRef()
和自建一個 {current:...}
對象的唯一區別是, useRef
會在每次渲染時返回同一個 ref 對象。
請記住,當 ref 對象內容發生變化時,useRef 並不會通知你。變更 .current 屬性不會引發組件重新渲染。如果想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則需要使用回調 ref 來實現。
在副作用裏面如何判定一個元素或者組件在本次渲染和上次渲染之間有過重新創建呢?
6. 自定義 Hooks
前面三篇,我們講到優化類組件的三大問題:
-
方便複用狀態邏輯
-
副作用的關注點分離
-
函數組件無
this
問題
對於組件的複用狀態沒怎麼說明,現在使用自定義 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 是一個函數,其名稱以 “use” 開頭,函數內部可以調用其他的 Hook」。我們在函數自定義寫法上似乎和編寫函數組件沒有區別,確實自定義組件與函數組件的最大區別就是**「輸入與輸出的區別」**。
再來一個特別的 Hook 加深一下映像。在上述代碼不變的條件下,我們在加一個自定義 Hook 內容如下:
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 與函數組件的相似性。
7.Hooks 使用法則
只在最頂層使用 Hook
「不要在循環,條件或嵌套函數中調用 Hook,」 確保總是在你的 React 函數的最頂層以及任何 return 之前調用他們。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調用。這讓 React 能夠在多次的 useState
和 useEffect
調用之間保持 hook 狀態的正確。(如果你對此感到好奇,我們在下面會有更深入的解釋。)
只在 React 函數中調用 Hook
** 不要在普通的 JavaScript 函數中調用 Hook。** 你可以:
-
✅ 在 React 的函數組件中調用 Hook
-
✅ 在自定義 Hook 中調用其他 Hook (我們將會在下一頁 中學習這個。)
遵循此規則,確保組件的狀態邏輯在代碼中清晰可見。
8.Hooks 使用過程中注意常見問題
官方文章上說的很明白,此處不在贅述:https://zh-hans.reactjs.org/docs/hooks-faq.html
小結
本文主要是對 React Hooks 的基本使用的介紹,其中的 DEMO 鏈接如下:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/index.js
所有 demo 都被引用在了一個 index.js 文件中,可根據文章介紹 demo 序號進行操作演示
查看本文不方便也可看掘金上已同步:https://juejin.cn/post/7008157923532767246/
參考
-
慕課《React 勁爆新特性 Hooks 重構旅遊電商網站火車票 PWA》
-
react 官網
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/P06gh0xPbiDv7GQvQPJeEQ