什么是 Hooks
以往,React 組件都是通過 class 的形式來編寫夭咬,只有無狀態(tài)組件才可以用函數(shù)來編寫啃炸。Hooks 就允許我們在函數(shù)組件中使用預(yù)定義函數(shù),來標(biāo)記狀態(tài)和組件生命周期皱埠,這使得所有組件都可以使用函數(shù)來編寫肮帐。
類組件的劣勢
- 狀態(tài)邏輯難復(fù)用
缺少復(fù)用機制
渲染屬性和高階組件導(dǎo)致層級冗余
- 狀態(tài)邏輯難復(fù)用
- 趨向復(fù)雜難以維護
生命周期函數(shù)混雜不相干邏輯
想干邏輯分散在不同生命周期
- 趨向復(fù)雜難以維護
- this指向困擾
內(nèi)聯(lián)函數(shù)過度創(chuàng)建新句柄
類成員函數(shù)不能保證this
- this指向困擾
優(yōu)化類組件的三大問題
- 函數(shù)組件無 this 問題
- 自定義 Hook 方便復(fù)用狀態(tài)邏輯
- 副作用的關(guān)注點分離
Hooks 使用法則
官方文檔: https://reactjs.org/docs/hooks-rules.html
- 僅在頂層調(diào)用 Hooks 函數(shù)
整個 Hooks 函數(shù)都依賴調(diào)用順序,這樣 React 才能在不同的渲染周期中把相同的邏輯關(guān)聯(lián)起來边器,一旦 Hooks 函數(shù)不在頂層調(diào)用训枢,那么很有可能在組件的不同渲染周期中,他們的調(diào)用順序發(fā)生了變化忘巧,進而導(dǎo)致變量混亂等錯誤恒界,為了盡量規(guī)避這些問題,所以盡量把 Hooks 放在最頂層砚嘴。
- 僅在函數(shù)組件及 函數(shù)組件 中調(diào)用 Hooks 函數(shù)十酣,不能放在普通函數(shù)及類組件中
Hooks 函數(shù)必須是use開頭
Hooks 常見問題
生命周期函數(shù)如何映射到 Hooks ?
getDerivedStateFromError() 這個處理錯誤的函數(shù)际长,暫時無法在 Hooks 中實現(xiàn)耸采,由此看出 Hooks 暫時無法代替類組件
function App () {
useEffect(() => {
// componentDidMount
return () => {
// componentWillUnmount
}
}, [])
let renderCouter = useRef(0) // 通過 ref 來保持渲染計數(shù)
renderCouter.current++
useEffect(() => {
if (renderCouter > 1) {
// componentDidUpdate
}
})
}
類實例成員變量如何映射到 Hooks ?
使用 useRef(0),同步到 current 中
Hooks 中如何獲取歷史 props 和 state
使用 useRef(0)工育,同步到 current 中
如何強制更新一個 Hooks 組件
創(chuàng)建一個不參與渲染的 state 的值虾宇,通過修改這個 state 的值來實現(xiàn)強制渲染
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
使用傳統(tǒng)類組件
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 的默認(rèn)值
// count 獲取值
// setCount 設(shè)置值
const [count, setCount] = useState(0)
return (
<button onClick={ () => { setCount(count + 1)} }>
HooksComponent Click Count ({count})
</button>
)
}
通過 props 設(shè)置默認(rèn)值
import React, { Component, useState } from 'react'
function HooksComponetDefault (props) {
// 使用 props 設(shè)置默認(rèn)值,useState 傳入的函數(shù)只會被調(diào)用一次
const [count, setCount] = useState(() => {
return props.defaultCount || 0
})
return (
<button onClick={ () => { setCount(count + 1)} }>
HooksComponent Click Count ({count})
</button>
)
}
使用 Effect Hooks
常見副作用:
- 綁定事件
- 網(wǎng)絡(luò)請求
- 訪問DoM
常見的副作用時機:
- Mount之后(componentDidMount)
- Update之后(componentDidUpdate)
- Unmount之前(componentWillUnmount)
以上的時機在傳統(tǒng)類組件中如绸,是在生命周期中解決嘱朽,在 hooks 中的解決方案:useEffect()
關(guān)于 useEffect()
userEffect()
標(biāo)準(zhǔn)上是在組件渲染(render)之后調(diào)用旭贬,并且根據(jù)自定義狀態(tài)來決定是否調(diào)用,函數(shù)組件第一次渲染后的調(diào)用搪泳,就相當(dāng)于componentDidMount()
稀轨,后面的調(diào)用都相當(dāng)于componentDidUpdate()
。
userEffect()
可以返回一個回調(diào)函數(shù)岸军,這個回調(diào)函數(shù)的執(zhí)行時機跟userEffect()
執(zhí)行時機掛鉤奋刽。這個回調(diào)函數(shù)的主要作用是 清除上一次副作用所遺留下來的狀態(tài)
useEffect() 的參數(shù)
【第一個參數(shù)】第一個參數(shù)為一個函數(shù),返回值為回調(diào)函數(shù)凛膏⊙蠲回調(diào)函數(shù)在視圖被銷毀的時候觸發(fā),1.組件重渲染猖毫,2.組件被卸載
【第二個參數(shù)】如果不填,則每次渲染后都會執(zhí)行须喂。第二個參數(shù)為一個數(shù)組吁断,只有數(shù)組的每一項都不變的情況下,userEffect()才不會執(zhí)行坞生,因此仔役,傳一個空數(shù)組的話,該 useEffect 就只會在第一次調(diào)用一次
簡單使用
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 第一個參數(shù)為一個函數(shù)
// 這里類似 componentDidount 和 componentDidUpdate
useEffect(() => {
document.title = count
})
// useEffect 第一個參數(shù)為一個函數(shù)是己,第二個參數(shù)為一個數(shù)組又兵,只有數(shù)組的每一項都不變的情況下,userEffect()才不會執(zhí)行
// 這里類似 componentDidMount 和 componentWillUnmount
useEffect(() => {
window.addEventListener('resize', onWindowResize, false)
// 回調(diào)函數(shù)在視圖被銷毀的時候觸發(fā)卒废,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環(huán)境下的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 中設(shè)置的是需要共享的值 */}
<CountContext.Provider value={count}>
<HooksContextConsumer />
</CountContext.Provider>
</div>
)
}
// 使用 useContext 調(diào)用摔认,參數(shù)為創(chuàng)建的 Provider
function HooksContextConsumer () {
const count = useContext(CountContext)
return (
<div>
HooksContextConsumer: <b>{count}</b>
</div>
)
}
使用 useMemo 和 memo
useMemo 和 memo 的區(qū)別逆皮,memo針對的是一個組件的渲染是否重復(fù)執(zhí)行,useMemo定義了一段函數(shù)是否重復(fù)執(zhí)行参袱。本質(zhì)都是利用同樣的算法电谣,來判斷依賴是否發(fā)生改變,進而決定是否觸發(fā)特定邏輯抹蚀,同樣都用來做性能優(yōu)化剿牺。
useMemo的參數(shù)
第一個參數(shù)為要執(zhí)行的邏輯函數(shù)
第二個參數(shù)為這個函數(shù)鎖依賴的變量組成的數(shù)組,如果不傳則 useMemo 的邏輯每次都執(zhí)行环壤,如果傳入空數(shù)組就執(zhí)行執(zhí)行一次
與 useEffect 的差異
兩個函數(shù)的調(diào)用時機不同晒来,useEffect() 執(zhí)行的是副作用,所以一定是在渲染完成之后運行镐捧,而useMemo() 是需要有返回值潜索,返回值直接參與渲染臭增,因為 useMemo() 是在渲染期間完成的。兩者存在一前一后的區(qū)別
useMemo 簡單使用
import React, { useState, useMemo } from 'react'
// 使用 memo
function HookMemoParent () {
const [ count, setCount ] = useState(0)
// 第一個參數(shù)為要執(zhí)行的邏輯函數(shù)
// 第二個參數(shù)為這個函數(shù)鎖依賴的變量組成的數(shù)組竹习,如果不傳則 useMemo 的邏輯每次都執(zhí)行誊抛,如果傳入空數(shù)組就執(zhí)行執(zhí)行一次
// 當(dāng)?shù)诙€參數(shù)發(fā)生變化時,就會觸發(fā)邏輯整陌,跟數(shù)組是什么值無關(guān)
const doubleCount = useMemo(() => {
return count * 2
}, [count])
// 根據(jù)第二個參數(shù)拗窃,count < 3 時,保持不變泌辫,不會重新計算
// 當(dāng)數(shù)組中的 bool 值發(fā)生改變時随夸,就會重新渲染,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
當(dāng)需要用 useMemo 返回一個函數(shù)時,可以使用 useCallback 代替殿遂,可以替代上一層函數(shù)诈铛。使用 useCallback 不能解決阻止創(chuàng)建新的函數(shù),因為每次組件的函數(shù)執(zhí)行都會創(chuàng)建新的函數(shù)墨礁,但是創(chuàng)建的這個函數(shù)不一定能夠被返回幢竹,很可能會被直接棄用。useCallback解決的是傳入子組件的函數(shù)參數(shù)過渡變化恩静,導(dǎo)致子組件過渡渲染的問題焕毫。實際上 useCallback 只是 useMemo 的一種簡寫。
【特別提醒】使用 useMemo 和 useCallback 時驶乾,當(dāng)依賴變化時邑飒,useMemo 和 useCallback 一定重新執(zhí)行。但是轻掩,當(dāng)依賴沒變化時幸乒,不能保證它們一定不執(zhí)行,也可能重新執(zhí)行唇牧,這是考慮內(nèi)存優(yōu)化的結(jié)果罕扎,react 官方的文檔中也沒有打包票一定不執(zhí)行。所以丐重,useMemo 和 useCallback 可以作為一種錦上添花的方案腔召,不可以過渡依賴它們是否重新執(zhí)行。
useCallback的參數(shù)
第一個參數(shù)為要執(zhí)行的邏輯函數(shù)
第二個參數(shù)為這個函數(shù)鎖依賴的變量組成的數(shù)組扮惦,如果不傳則 useMemo 的邏輯每次都執(zhí)行臀蛛,如果傳入空數(shù)組就執(zhí)行執(zhí)行一次
useMemo(() => {
return () => { console.log("click") }
}, [])
// 上下兩者等價
useCallback(() => {
console.log("click")
}, [])
使用 useRef
- 獲取子組件或者DOM節(jié)點的句柄
- 獲取跨越渲染周期的任意數(shù)據(jù)
useRef 和 useState 區(qū)別
state 的賦值會觸發(fā)組件重渲染,但是 ref 不會
【特別用法】
如果在組件中,需要使用上次渲染的數(shù)據(jù)浊仆,可以使用useRef客峭,同步到 current 中
// useRef
function HooksUseRef () {
const [ count, setCount ] = useState(0)
// 聲明ref
const countRef = useRef()
// 將定時器實例,通步在useRef中抡柿,這樣就可以防止每次渲染時重復(fù)調(diào)用
const timer = useRef()
// 給子組件傳遞的事件
const bindChildClick = useCallback(() => {
console.log('click')
// 通過 current 獲取 DOM 節(jié)點
console.log(countRef.current)
// 通過 ref 調(diào)用子組件的方法
countRef.current.speak()
}, [countRef])
// 使 count 自增
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
cleanup
}
}, [])
// 監(jiān)測 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 函數(shù)
// 自定義 hooks 函數(shù),一定要使用 use 開頭
function useCount (defaultCount) {
const [count, setCount] = useState(defaultCount)
const timer = useRef()
// 使 count 自增
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1)
}, 1000)
return () => {
cleanup
}
}, [])
// 監(jiān)測 count 大于10洲劣,停止自增
useEffect(() => {
if (count >= 10) {
clearInterval(timer.current)
}
})
// 這里的返回值可以自定義备蚓,我參考 useState 的返回
return [count, setCount]
}
// 自定義 hooks 函數(shù),返回 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>
)
}