什麼是 Hooks
以往,React 元件都是通過 class 的形式來編寫,只有無狀態元件才可以用函式來編寫。Hooks 就允許我們在函式元件中使用預定義函式,來標記狀態和元件生命週期,這使得所有元件都可以使用函式來編寫。
類元件的劣勢
-
- 狀態邏輯難複用 缺少複用機制 渲染屬性和高階元件導致層級冗餘
-
- 趨向複雜難以維護 生命週期函式混雜不相干邏輯 想幹邏輯分散在不同生命週期
-
- this指向困擾 行內函數過度建立新控制程式碼 類成員函式不能保證this
優化類元件的三大問題
- 函式元件無 this 問題
- 自定義 Hook 方便複用狀態邏輯
- 副作用的關注點分離
Hooks 使用法則
官方文件: https://reactjs.org/docs/hooks-rules.html
- 僅在頂層呼叫 Hooks 函式
整個 Hooks 函式都依賴呼叫順序,這樣 React 才能在不同的渲染週期中把相同的邏輯關聯起來,一旦 Hooks 函式不在頂層呼叫,那麼很有可能在元件的不同渲染週期中,他們的呼叫順序發生了變化,進而導致變數混亂等錯誤,為了儘量規避這些問題,所以儘量把 Hooks 放在最頂層。
- 僅在函式元件及 函式元件 中呼叫 Hooks 函式,不能放在普通函式及類元件中
Hooks 函式必須是use開頭
Hooks 常見問題
生命週期函式如何對映到 Hooks ?
getDerivedStateFromError() 這個處理錯誤的函式,暫時無法在 Hooks 中實現,由此看出 Hooks 暫時無法代替類元件
function App () {
useEffect(() => {
// componentDidMount
return () => {
// componentWillUnmount
}
}, [])
let renderCouter = useRef(0) // 通過 ref 來保持渲染計數
renderCouter.current++
useEffect(() => {
if (renderCouter > 1) {
// componentDidUpdate
}
})
}
複製程式碼
類例項成員變數如何對映到 Hooks ?
使用 useRef(0),同步到 current 中
Hooks 中如何獲取歷史 props 和 state
使用 useRef(0),同步到 current 中
如何強制更新一個 Hooks 元件
建立一個不參與渲染的 state 的值,通過修改這個 state 的值來實現強制渲染
function Counter () {
const [updater, setUpdater] = useState(0)
function forceUpdate () {
setUpdater(updater => updater + 1)
}
}
複製程式碼
使用 eslint-plugin-react-hooks 防止程式碼出錯
1.安裝
npm install eslint-plugin-react-hooks -D
複製程式碼
2.在package.json 中配置
// package.json
...
"eslintConfig": {
"extends": "react-app",
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error"
}
},
...
複製程式碼
簡單使用 useState
使用傳統類元件
import React, { Component } from 'react'
class ClassComponent extends Component {
state = {
count: 0
}
render () {
const { count } = this.state
return (
<button onClick={() => {this.setState({ count: count + 1 })}}>
ClassComponent Click Count ({count})
</button>
)
}
}
複製程式碼
使用 Hooks
import React, { Component, useState } from 'react'
function HooksComponent () {
// useState 傳入 count 的預設值
// count 獲取值
// setCount 設定值
const [count, setCount] = useState(0)
return (
<button onClick={ () => { setCount(count + 1)} }>
HooksComponent Click Count ({count})
</button>
)
}
複製程式碼
通過 props 設定預設值
import React, { Component, useState } from 'react'
function HooksComponetDefault (props) {
// 使用 props 設定預設值,useState 傳入的函式只會被呼叫一次
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
return (
<button onClick={ () => { setCount(count + 1)} }>
HooksComponent Click Count ({count})
</button>
)
}
複製程式碼
使用 Effect Hooks
常見副作用:
- 繫結事件
- 網路請求
- 訪問DoM
常見的副作用時機:
- Mount之後(componentDidMount)
- Update之後(componentDidUpdate)
- Unmount之前(componentWillUnmount)
以上的時機在傳統類元件中,是在生命週期中解決,在 hooks 中的解決方案:useEffect()
關於 useEffect()
userEffect()
標準上是在元件渲染(render)之後呼叫,並且根據自定義狀態來決定是否呼叫,函式元件第一次渲染後的呼叫,就相當於componentDidMount()
,後面的呼叫都相當於componentDidUpdate()
。
userEffect()
可以返回一個回撥函式,這個回撥函式的執行時機跟userEffect()
執行時機掛鉤。這個回撥函式的主要作用是 清除上一次副作用所遺留下來的狀態
useEffect() 的引數
【第一個引數】第一個引數為一個函式,返回值為回撥函式。回撥函式在檢視被銷燬的時候觸發,1.元件重渲染,2.元件被解除安裝
【第二個引數】如果不填,則每次渲染後都會執行。第二個引數為一個陣列,只有陣列的每一項都不變的情況下,userEffect()才不會執行,因此,傳一個空陣列的話,該 useEffect 就只會在第一次呼叫一次
簡單使用
function HooksCompinentUseEffect () {
const [ count, setCount ] = useState(0)
const [ size, setSize ] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
const onWindowResize = () => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}
// useEffect 第一個引數為一個函式
// 這裡類似 componentDidount 和 componentDidUpdate
useEffect(() => {
document.title = count
})
// useEffect 第一個引數為一個函式,第二個引數為一個陣列,只有陣列的每一項都不變的情況下,userEffect()才不會執行
// 這裡類似 componentDidMount 和 componentWillUnmount
useEffect(() => {
window.addEventListener('resize', onWindowResize, false)
// 回撥函式在檢視被銷燬的時候觸發,1.元件重渲染,2.元件被解除安裝
return () => {
window.removeEventListener('resize', onWindowResize, false)
}
}, [])
return (
<div>
<button onClick={() => { setCount(count + 1)} }>
HooksCompinentUseEffect Click Count ({count})
</button>
W: {size.width} | H: {size.height}
</div>
)
}
複製程式碼
Hooks環境下的Context
不要濫用 context,因為會破壞元件的獨立性
import React, { useState, createContext, useContext } from 'react'
// 使用 context
const CountContext = createContext()
function HooksContextProvider () {
const [ count, setCount ] = useState(0)
return (
<div>
<button onClick={() => { setCount(count + 1) }}>HooksContextProvider {count}</button>
{/* value 中設定的是需要共享的值 */}
<CountContext.Provider value={count}>
<HooksContextConsumer />
</CountContext.Provider>
</div>
)
}
// 使用 useContext 呼叫,引數為建立的 Provider
function HooksContextConsumer () {
const count = useContext(CountContext)
return (
<div>
HooksContextConsumer: <b>{count}</b>
</div>
)
}
複製程式碼
使用 useMemo 和 memo
useMemo 和 memo 的區別,memo針對的是一個元件的渲染是否重複執行,useMemo定義了一段函式是否重複執行。本質都是利用同樣的演算法,來判斷依賴是否發生改變,進而決定是否觸發特定邏輯,同樣都用來做效能優化。
useMemo的引數
第一個引數為要執行的邏輯函式
第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次
與 useEffect 的差異
兩個函式的呼叫時機不同,useEffect() 執行的是副作用,所以一定是在渲染完成之後執行,而useMemo() 是需要有返回值,返回值直接參與渲染,因為 useMemo() 是在渲染期間完成的。兩者存在一前一後的區別
useMemo 簡單使用
import React, { useState, useMemo } from 'react'
// 使用 memo
function HookMemoParent () {
const [ count, setCount ] = useState(0)
// 第一個引數為要執行的邏輯函式
// 第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次
// 當第二個引數發生變化時,就會觸發邏輯,跟陣列是什麼值無關
const doubleCount = useMemo(() => {
return count * 2
}, [count])
// 根據第二個引數,count < 3 時,保持不變,不會重新計算
// 當陣列中的 bool 值發生改變時,就會重新渲染,false => true => false,所以 boolCount 會重新渲染兩次
const boolCount = useMemo(() => {
return count * 3
}, [count === 3])
return (
<div>
<button onClick={() => {setCount(count + 1)}}>HookMemoParent: {count},doubleCount: {doubleCount}, boolCount: {boolCount}</button>
<HooksMemoChild count={doubleCount}></HooksMemoChild>
</div>
)
}
function HooksMemoChild (props) {
return (
<div>{props.count}</div>
)
}
複製程式碼
useMemo 和 useCallback
當需要用 useMemo 返回一個函式時,可以使用 useCallback 代替,可以替代上一層函式。使用 useCallback 不能解決阻止建立新的函式,因為每次元件的函式執行都會建立新的函式,但是建立的這個函式不一定能夠被返回,很可能會被直接棄用。useCallback解決的是傳入子元件的函式引數過渡變化,導致子元件過渡渲染的問題。實際上 useCallback 只是 useMemo 的一種簡寫。
【特別提醒】使用 useMemo 和 useCallback 時,當依賴變化時,useMemo 和 useCallback 一定重新執行。但是,當依賴沒變化時,不能保證它們一定不執行,也可能重新執行,這是考慮記憶體優化的結果,react 官方的文件中也沒有打包票一定不執行。所以,useMemo 和 useCallback 可以作為一種錦上添花的方案,不可以過渡依賴它們是否重新執行。
useCallback的引數
第一個引數為要執行的邏輯函式
第二個引數為這個函式鎖依賴的變數組成的陣列,如果不傳則 useMemo 的邏輯每次都執行,如果傳入空陣列就執行執行一次
useMemo(() => {
return () => { console.log("click") }
}, [])
// 上下兩者等價
useCallback(() => {
console.log("click")
}, [])
複製程式碼
使用 useRef
- 獲取子元件或者DOM節點的控制程式碼
- 獲取跨越渲染週期的任意資料
useRef 和 useState 區別
state 的賦值會觸發元件重渲染,但是 ref 不會
【特別用法】
如果在元件中,需要使用上次渲染的資料,可以使用useRef,同步到 current 中
// useRef
function HooksUseRef () {
const [ count, setCount ] = useState(0)
// 宣告ref
const countRef = useRef()
// 將定時器例項,通步在useRef中,這樣就可以防止每次渲染時重複呼叫
const timer = useRef()
// 給子元件傳遞的事件
const bindChildClick = useCallback(() => {
console.log('click')
// 通過 current 獲取 DOM 節點
console.log(countRef.current)
// 通過 ref 呼叫子元件的方法
countRef.current.speak()
}, [countRef])
// 使 count 自增
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
cleanup
}
}, [])
// 監測 count 大於10,停止自增
useEffect(() => {
if (count >= 10) {
clearInterval(timer.current)
}
})
return (
<div>
<button onClick={() => {setCount(count + 1)}}>HooksUseRef: {count}</button>
{/* 給類元件的ref屬性賦值 */}
<HooksUseRefClassChild ref={countRef} count={count} onClick={bindChildClick}></HooksUseRefClassChild>
</div>
)
}
// 類元件獲取 ref
class HooksUseRefClassChild extends PureComponent {
speak () {
console.log('this is child function: ', this.props.count)
}
render () {
const { props } = this
return <b onClick={props.onClick}> HooksUseRefClassChild: {props.count}</b>
}
}
複製程式碼
自定義 Hooks 函式
// 自定義 hooks 函式,一定要使用 use 開頭
function useCount (defaultCount) {
const [count, setCount] = useState(defaultCount)
const timer = useRef()
// 使 count 自增
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
cleanup
}
}, [])
// 監測 count 大於10,停止自增
useEffect(() => {
if (count >= 10) {
clearInterval(timer.current)
}
})
// 這裡的返回值可以自定義,我參考 useState 的返回
return [count, setCount]
}
// 自定義 hooks 函式,返回 JSX
function useCountJSX (defaultCount) {
return (
<b>{defaultCount}</b>
)
}
function HooksSelfFunciton () {
const [count, setCount] = useCount(0) // 模仿原生的 useState
const CountJSX = useCountJSX(count) // 直接返回 JSX
return (
<div>
<button onClick={() => {setCount(count + 1)}}>useCount: {count}</button>
useCountJSX: {CountJSX}
</div>
)
}
複製程式碼