1. React Hooks 是什么
React Hooks 是 React V16.8 版本新增的特性,即在不編寫類組件的情況下使用 state 以及 React 的新特性。React 官網(wǎng)提供了 10 個 Hooks API,來滿足我們在函數(shù)組件中定義狀態(tài)墙牌,提供類似生命周期的功能和一些高級特性拓轻。
2. Hooks 的誕生背景
2.1. 類組件的不足
- 狀態(tài)邏輯難以復(fù)用:
在舊版本的 React 中,想要實(shí)現(xiàn)邏輯的復(fù)用,需要使用到HOC
或者Render Props
啡邑,增加了組件的使用層級产徊,同時學(xué)習(xí)使用成本也比較高嫩海。 - 使用趨于復(fù)雜且維護(hù)成本較高
有多個監(jiān)聽狀態(tài)的生命周期,同一個功能的整個過程可能要在不同的生命周期完成囚痴,不夠統(tǒng)一叁怪;尤其是引入 Redux 后,會變得復(fù)雜深滚,維護(hù)成本較高奕谭。 - this 綁定問題
在類組件中如果不使用箭頭函數(shù),需要顯示的綁定 this痴荐,容易造成 this 丟失血柳,導(dǎo)致數(shù)據(jù)混亂。
2.2. Hooks 的優(yōu)勢
- 自定義 Hooks 可以實(shí)現(xiàn)公共的邏輯抽離生兆,便于復(fù)用
- 可以將組件抽成更小的函數(shù)單元难捌,實(shí)現(xiàn)一個函數(shù)只關(guān)注一個功能,更加清晰
- 更加豐富的性能優(yōu)化手段
- 組件樹層級變淺鸦难,使用 HOC/Render Props 實(shí)現(xiàn)組件的狀態(tài)復(fù)用根吁,會增加組件的層級,但 Hooks 無需增加層級即可實(shí)現(xiàn)合蔽。
3. 10 個官方 Hooks 案例詳解
3.1. useState
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Button,Modal } from 'antd'
/**
* useState:定義組件的狀態(tài)
* 作用:
* 通過傳入 `useState` 參數(shù)后返回一個帶有默認(rèn)狀態(tài)和改變狀態(tài)函數(shù)的數(shù)組击敌。通過傳入新狀態(tài)給函數(shù)來改變原本的狀態(tài)值。
*/
// 類組件寫法
class Example extends React.Component {
constructor() {
super()
this.state = { count: 0}
}
render() {
return (
<div>
<div>你點(diǎn)擊了{(lán)this.state.count}次</div>
<button onClick={() => this.setState({count: this.state.count +1})}>點(diǎn)擊</button>
</div>
)
}
}
// hooks 寫法
function Example1() {
// 定義一個count變量拴事,賦初始值0
const [count,setCount] = useState(0)
return (
<div>
<div>你點(diǎn)擊了{(lán)count}次</div>
<button onClick={() => setCount(count +1 )}>點(diǎn)擊</button>
</div>
)
}
// setCount 接收函數(shù)作為參數(shù)
function Example2() {
const [count,setCount] = useState(0)
// preCount 參數(shù)為上一次的值
const countAction = (preCount,a) => preCount + a
return (
<div>
<div>你點(diǎn)擊了{(lán)count}次</div>
<button onClick={() => setCount(countAction(count,1))}>點(diǎn)擊</button>
</div>
)
}
/**
* 2 . renderProps 和 hooks 的比較沃斤。徹底理解 hooks 的價值和優(yōu)點(diǎn)。
*/
// renderProps 抽離公共邏輯
class Toggle extends React.Component {
// 定義默認(rèn)屬性
state= { on: false}
constructor(props) {
super(props)
// 接收父組件傳遞的參數(shù)
this.state.on = this.props.initial
}
toggle = () => {
this.setState({ on: !this.state.on })
}
render() {
// 向子組件傳遞了屬性和方法
return this.props.children(this.state.on,this.toggle)
}
}
function Example3() {
return (
<Toggle initial={false}>
{/* 通過一個方法接收參數(shù) */}
{
(on,toggle) => (
<React.Fragment>
<Button type="primary" onClick={toggle}>打開彈框</Button>
<Modal visible={on} onOk={toggle} onCancel={toggle}>我是彈框</Modal>
</React.Fragment>
)
}
</Toggle>
)
}
// hooks 寫法 - 優(yōu)勢:多個狀態(tài)不會產(chǎn)生嵌套
function Example4 () {
const [visible,setVisible] = useState(false)
return (
<div>
<Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
<Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
</div>
)
}
const App = props => <div>
<Example />
<hr />
<Example1 />
<hr/>
<Example2/>
<hr />
<Example3 />
<hr />
<Example4 />
</div>
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
3.2. useEffect
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { Button,Modal } from 'antd'
/**
* useEffect: 處理副作用(副作用:指那些沒有發(fā)生在數(shù)據(jù)向視圖轉(zhuǎn)換過程中的邏輯刃宵,如 ajax 請求衡瓶、訪問原生dom 元素、本地持久化緩存牲证、綁定/解綁事件哮针、添加訂閱、設(shè)置定時器、記錄日志等诚撵。)
* 作用: 函數(shù)組件能保存狀態(tài)缭裆,但是對于異步請求,副作用的操作還是無能為力寿烟,所以 React 提供了 useEffect 來幫助開發(fā)者處理函數(shù)組件的副作用澈驼,類似生命周期函數(shù),相當(dāng)于是 componentDidMount筛武,componentDidUpdate 和 componentWillUnmount 這三個函數(shù)的組合缝其,可以通過傳參及其他邏輯,分別模擬*這三個生命周期函數(shù)徘六。
* useEffect具有以下5個特性:
* 1. 第一次渲染時執(zhí)行内边,任何狀態(tài)發(fā)生變化都執(zhí)行 - 只指定一個回調(diào)函數(shù)作為參數(shù), 相當(dāng)于componentDidMount & componentDidUpdate
* 2. 第一次渲染執(zhí)行待锈,任何狀態(tài)發(fā)生變化時不執(zhí)行
* 3. 第一次渲染執(zhí)行漠其,通過第二個參數(shù)指定狀態(tài)發(fā)生變化時執(zhí)行,其他狀態(tài)發(fā)生變化不執(zhí)行
* 4. 監(jiān)聽多個狀態(tài)時竿音,可以同時定義多個useEffect
* 5. 組件卸載時會執(zhí)行回調(diào)函數(shù)返回的回調(diào)函數(shù) - 相當(dāng)于componentWillUnmount
* 6. 未傳遞第二個參數(shù)和屎,所有狀態(tài)更新就執(zhí)行useEffect,或者指定狀態(tài)春瞬,對應(yīng)狀態(tài)更新執(zhí)行useEffect時柴信,會先執(zhí)行返回值回調(diào),再執(zhí)行第一個回調(diào)參數(shù)(第二個參數(shù)為空數(shù)組時任何狀態(tài)更新都不會執(zhí)行)
*
/**
* 1. useEffect只有一個回調(diào)函數(shù)作為第一個參數(shù)時:
* 1.1.初始化時會執(zhí)行一次回調(diào)函數(shù)
* 1.2.任一一個狀態(tài)數(shù)據(jù)發(fā)生變化時都會執(zhí)行回調(diào)函數(shù)
*/
function Example () {
const [count,setCount] = useState(0)
useEffect(() => {
// 初始化時執(zhí)行一次宽气,count每次變化的時候都會執(zhí)行
console.log('我執(zhí)行啦随常!')
})
return (
<div>
<div>點(diǎn)擊了{(lán)count}次</div>
<Button type='primary' onClick={() => setCount(count+1)}>點(diǎn)擊</Button>
</div>
)
}
/**
* 2. useEffect傳入兩個參數(shù):第一個參數(shù)是回調(diào)函數(shù),第二個參數(shù)是空數(shù)組:
* useEffect的回調(diào)函數(shù)只會在初始化渲染時執(zhí)行一次
*/
function Example1() {
const [count,setCount] = useState(0)
useEffect(() => {
// 只會在初次渲染時執(zhí)行萄涯,任何狀態(tài)數(shù)據(jù)發(fā)生變化都不會執(zhí)行
console.log('我執(zhí)行啦111111绪氛!')
},[])
return (
<div>
<div>你點(diǎn)擊了{(lán)count}次</div>
<Button type='primary' onClick={() => setCount(count + 1)}>點(diǎn)擊</Button>
</div>
)
}
/**
* 3. useEffect 傳入兩個參數(shù),第一個是回調(diào)函數(shù)窃判,第二個是指定數(shù)據(jù)的數(shù)組
* 3.1 初次渲染時執(zhí)行一次回調(diào)函數(shù)
* 3.2 指定數(shù)據(jù)發(fā)生變化時執(zhí)行一次回調(diào)函數(shù)
*/
function Example2() {
const [visible,setVisible] = useState(false)
const [count,setCount] = useState(0)
useEffect(() => {
// 初始渲染時會執(zhí)行一次钞楼,visible狀態(tài)發(fā)生變化時會執(zhí)行,count發(fā)生變化時則不會執(zhí)行
console.log('我最帥了')
},[visible])
return (
<div>
<div>點(diǎn)擊了{(lán)count}次</div>
<Button type='primary' onClick={() => setCount(count +1) }>點(diǎn)擊</Button>
<Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
<Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
</div>
)
}
/**
* 4. 監(jiān)聽多個狀態(tài)發(fā)生變化時執(zhí)行useEffect的回調(diào)函數(shù)時袄琳,可以同時使用多個useEffect
*/
function Example3() {
const [visible,setVisible] = useState(false)
const [count,setCount] = useState(0)
useEffect(() => {
// 初始渲染的時候執(zhí)行一次,count狀態(tài)發(fā)生變化時會執(zhí)行
console.log('我是count')
},[count])
useEffect(() => {
// 初始渲染時會執(zhí)行一次燃乍,visible狀態(tài)發(fā)生變化時會執(zhí)行唆樊,count發(fā)生變化時則不會執(zhí)行
console.log('我是彈框')
},[visible])
return (
<div>
<div>點(diǎn)擊了{(lán)count}次</div>
<Button type='primary' onClick={() => setCount(count +1) }>點(diǎn)擊</Button>
<Button type='primary' onClick={() => setVisible(true)}>打開彈框</Button>
<Modal visible={visible} onOk={() => setVisible(false)} onCancel={() => setVisible(false)}>我是彈框內(nèi)容</Modal>
</div>
)
}
/**
* 5. useEffect的回調(diào)函數(shù)的返回值(回調(diào)函數(shù))執(zhí)行時機(jī):
* ① 組件銷毀時
* ② 未傳遞第二個參數(shù),所有狀態(tài)更新就執(zhí)行useEffect刻蟹,或者指定狀態(tài)逗旁,對應(yīng)狀態(tài)更新時,會先執(zhí)行返回值回調(diào),再執(zhí)行第一個回調(diào)參數(shù)
* ps: 如果指定第二個參數(shù)為空數(shù)組時狀態(tài)更新還是不會執(zhí)行的
*/
function Test() {
const [ count, setCount ] = useState(0)
useEffect(() => {
console.log('Test組件渲染更新了')
return () => {
// 組件卸載時執(zhí)行
// 狀態(tài)更新執(zhí)行第一個參數(shù)回調(diào)前會先執(zhí)行
console.log('Test組件銷毀了')
}
},[count])
return (
<div>
<Button type='primary' onClick={() => setCount(count + 1)}>點(diǎn)擊</Button>
<div>測試子組件點(diǎn)擊了{(lán)count}次數(shù)</div>
</div>
)
}
function Example4 () {
const [show,setShow] = useState(true)
return (
<div>
<Button type='primary' onClick={() => setShow(!show)}>顯示/關(guān)閉</Button>
{
show ? <Test /> : null
}
</div>
)
}
const App = props => {
return (
<div>
<Example />
<hr />
<Example1 />
<hr />
<Example2 />
<hr />
<Example3 />
<hr />
<Example4 />
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
3.3. useContext
import React, { useContext, useState } from "react";
import ReactDOM from 'react-dom';
/**
* useContext: 減少組件層級
* 是類組件的的context的的hooks版片效,主要用于在父組件中公共數(shù)據(jù)和邏輯的抽離红伦,方便子組件公用。
*/
// 1. 創(chuàng)建Context對象
const ThemeContext = React.createContext()
// 2. Provider 組件淀衣,發(fā)布數(shù)據(jù)昙读,向所有的子組件提供數(shù)據(jù)
const App = props => {
const [theme,setTheme] = useState('green')
return (
// Provider 發(fā)送數(shù)據(jù),兩個屬性膨桥,用對象的形式
<ThemeContext.Provider value={{theme,setTheme}}>
<div>
<Toolbar />
</div>
</ThemeContext.Provider>
)
}
// 中間組件
const Toolbar = props => {
return (
<div>
<ThemedButton />
</div>
)
}
// 3. 子孫組件使用useContext 接收收據(jù)
const ThemedButton = (props) => {
// useContext接收頂層組件傳遞過來的context數(shù)據(jù)蛮浑, 傳遞過來是對象,就用對象結(jié)構(gòu)接收
const { theme,setTheme }= useContext(ThemeContext)
return (
<div>
{/* 可以直接使用接收到的數(shù)據(jù)和方法 */}
<div style={{ 'color': theme }}>Theme: {theme}</div>
<button onClick={() => setTheme('red')}>切換主題</button>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
3.4. useReducer
import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';
/**
* useReducer: 就是子當(dāng)以useState執(zhí)行了比較復(fù)雜的state更新
* 以hook的方式定了新的全局狀態(tài)管理只嚣,可以用來替代redux(實(shí)際為同一個作者)
*/
// 接收派發(fā)的action沮稚,執(zhí)行對state進(jìn)行更改
function reducer(state,action) {
// 傳入舊的state,返回新的state
switch (action.type) {
case 'reset':
return { count: action.payload }
case 'increment':
return { count: state.count + 1}
case 'decrement':
return { count: state.count - 1}
default:
return state
}
}
// 允許對初始state執(zhí)行二次變更
function init(initialCountState) {
return { count : initialCountState.count + 1}
}
function Counter({initialCount}) {
// state, dispatch 是useReducer返回的內(nèi)容
const [state, dispatch] = useReducer(
reducer,// 派發(fā)action 執(zhí)行state修改
initialCount, // 傳遞給state的初始值
init // 可選參數(shù)册舞,允許對初始state進(jìn)行二次變更
)
return (
<React.Fragment>
<div>Count: {state.count}</div>
{/* 執(zhí)行dispatch派發(fā)變更state的action */}
<button onClick={() => dispatch({ type: 'reset', payload: initialCount.count })}>重置</button>
<button onClick={() => dispatch({type: 'increment'})}>增加</button>
<button onClick={() => dispatch({type: 'decrement'})}>減少</button>
</React.Fragment>
)
}
const App = props => {
const initialCountState = {count: 0}
return (
<div>
<Counter initialCount={initialCountState}/>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
3.5. useCallback
import React, { useCallback, useState,memo } from "react";
import ReactDOM from "react-dom";
import { Modal } from 'antd'
/**
* useCallback: 記憶函數(shù)
* 作用:性能優(yōu)化蕴掏,避免重復(fù)的創(chuàng)建引用和重復(fù)無意義的組件渲染,加大性能開銷调鲸,對于一些開銷昂貴的組件來說是很好的優(yōu)化手段囚似。
* 特性:
* 1. useCallback 會將第一個函數(shù)參數(shù)作為回調(diào)函數(shù)返回,使用useCallback優(yōu)化過的回調(diào)函數(shù)线得,會在組件初始化渲染時創(chuàng)建函數(shù)對象并生成引用饶唤,之后組件再次更新渲染時則不會再次創(chuàng)建新對象和引用(普通函數(shù)每次組件更新都會創(chuàng)新新的函數(shù)對象并生成引用)
* 2. useCallback可以通過傳遞第二個參數(shù),控制對應(yīng)的狀態(tài)數(shù)據(jù)發(fā)生變化時才重新創(chuàng)建對象并生成新的引用贯钩,默認(rèn)值時空數(shù)組[]募狂,即不監(jiān)控狀態(tài)數(shù)據(jù)
*/
/**
* 案例:
*/
// 1. 未使用useCallback的組件函數(shù),每次數(shù)據(jù)更新時都會重復(fù)創(chuàng)建函數(shù)對象生成新的引用
let fn = null
const Example1 = ({count,setCount}) => {
// 組件內(nèi)普通函數(shù)
const ordinaryCallback = () => {
console.log('我是函數(shù)函數(shù)')
}
// 狀態(tài)發(fā)生變化角雷,組件渲染祸穷,一直都返回false => 表明每次渲染都會創(chuàng)建新的函數(shù)對象,產(chǎn)生新的引用
console.log('是否是Example1的同一個回調(diào)函數(shù):',Object.is(fn,ordinaryCallback)) // false
fn = ordinaryCallback
return (
<div>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>Count:{count}</div>
</div>
)
}
// 2. 使用useCallback優(yōu)化的組件函數(shù)勺三,組件重新渲染時不會重新創(chuàng)建函數(shù)對象
let fn1 = null
const Example2 = ({count,setCount}) => {
// 使用useCallback 優(yōu)化后的組件函數(shù)雷滚,組件重復(fù)渲染時不會重復(fù)創(chuàng)新函數(shù)對象
const memoizedCallback = useCallback(() => {
console.log('我是組件函數(shù)')
},[])
// 狀態(tài)更新時,組件重新渲染吗坚,初次渲染返回false祈远,更新渲染一直返回true => 表示經(jīng)過useCallback優(yōu)化后的函數(shù),在組件更新渲染時不會重復(fù)創(chuàng)建函數(shù)對象商源,依舊保持第一次創(chuàng)建時的引用
console.log('是否是Example2的同一個回調(diào)函數(shù):', Object.is(fn1,memoizedCallback)) // 初始渲染是false车份,之后一直是true
fn1 = memoizedCallback
return (
<div>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>Count: {count}</div>
</div>
)
}
// 3. 通過useCallback的第二個參數(shù)控制指定狀態(tài)數(shù)據(jù)更新,組件重新渲染時牡彻,再創(chuàng)建新的函數(shù)對象 (感覺沒啥卵用)
let fn3 = null
function Example3({count,setCount}) {
const [visible, setVisible] = useState(false)
// 設(shè)定只有visible發(fā)生變化組件更新時才創(chuàng)新創(chuàng)建函數(shù)對象扫沼,其他情況下渲染不會重新創(chuàng)建
const memoizedCallback = useCallback(() => {
console.log('我是組件函數(shù)')
},[visible])
// count狀態(tài)發(fā)生變化時返回true => 表示不會重新創(chuàng)建函數(shù)
// visible狀態(tài)發(fā)生變化時返回false => 表示會重新創(chuàng)建函數(shù)
console.log('是否是Example3的同一個回調(diào)函數(shù):', Object.is(fn3,memoizedCallback))
fn3 = memoizedCallback
return (
<div>
<button onClick={() => setCount(count + 1)}>增加數(shù)量</button>
<div>Count: {count}</div>
<button onClick={() => setVisible(!visible)}>顯示/隱藏彈框</button>
<Modal visible={visible} onOk={() => setVisible(!visible)} onCancel={() => setVisible(!visible)}/>
</div>
)
}
// 4. 綜合案例
// 昂貴開銷的組件
const ExpensiveComponent = memo(({fn}) => {
// 初始化時執(zhí)行,p1狀態(tài)更新時會執(zhí)行,p2狀態(tài)更新時則不會
console.log('我被迫渲染啦6谐Q暇汀!')
return <div onClick={fn}>我是一個渲染消耗昂貴的組件</div>
})
let fnn1 = null
const Child1 = () => {
const fn1 = () => console.log('fn1')
console.log('fnn1: ', Object.is(fnn1,fn1))
fnn1 = fn1
return <div>
<ExpensiveComponent fn={fn1}/>
</div>
}
let fnn2 = null
const Child2 = () => {
// 使用useCallback 不會重復(fù)創(chuàng)建函數(shù)對象器罐,fn2不會重復(fù)創(chuàng)建增加ExpensiveComponent組件的重復(fù)渲染
const fn2 = useCallback(() => console.log('fn2'),[])
console.log('fnn2: ', Object.is(fnn2,fn2))
fnn2 = fn2
return <div>
{/* p2狀態(tài)改變不會導(dǎo)致fn2引用變化梢为,因此該組件不會重復(fù)渲染 */}
<ExpensiveComponent fn={fn2}/>
</div>
}
const Example4 = () => {
const [p1, setP1] = useState(0)
const [p2, setP2] = useState(0)
return (
<div>
<h4>每次點(diǎn)擊fn4都是新的</h4>
<Child1 p1={p1}/>
<button onClick={() => setP1(p1 + 1)}>按鈕1</button>
<hr />
<h4>每次點(diǎn)擊fn4不重新生成</h4>
<Child2 p2={p2}/>
<button onClick={() => setP2(p2 + 1)}>按鈕2</button>
</div>
)
}
const App = props => {
const [count,setCount] = useState(0)
return (
<div>
<Example1 count={count} setCount={setCount}/>
<hr />
<Example2 count={count} setCount={setCount}/>
<hr />
<Example3 count={count} setCount={setCount}/>
<hr />
<Example4 />
</div>
)
}
ReactDOM.render(<App/>,document.getElementById('root'))
3.6. useMemo
import React, { useMemo, useState } from "react";
import ReactDOM from 'react-dom'
/**
* useMemo 記憶組件
* 作用:可以保存組件的渲染結(jié)果,根據(jù)條件確實(shí)是否重新渲染,主要是用來進(jìn)行性能優(yōu)化
* 特性:
* 1. 使用useMemo包括的jsx代碼段初次渲染后技矮,會將渲染結(jié)果保存抖誉,組件再次更新時不會重復(fù)渲染
* 2. 指定狀態(tài)條件發(fā)生變化時,才會進(jìn)行重新渲染
* 場景:
* 在一些復(fù)雜計算的代碼段中衰倦,可能并不依賴很多組件狀態(tài)袒炉,如果任一狀態(tài)發(fā)生變化時都重新渲染,是很大的性能開銷樊零,此時就可以使用useMemo我磁,將這樣的代碼段包裹起來,
* 只有依賴的狀態(tài)發(fā)生變化時才會重新渲染驻襟,可以進(jìn)行組件的性能提升夺艰。
*/
const Child = ({c}) => {
console.log('Child重新渲染',c)
return (
<div>{c}</div>
)
}
const Parent = ({a,b}) => {
// a,b發(fā)生改變時都會重新渲染
const child1 = <div>
{ console.log('這是一個復(fù)雜的計算child1')}
<Child c={a}/>
</div>
// 初次渲染,之后只有b發(fā)生改變時才會重新渲染沉衣,否則保留上一次渲染的結(jié)果
const child2 = useMemo(() =>
<div>
{ console.log('這是一個復(fù)雜的計算child2')}
<Child c=郁副/>
</div>,
[b]
)
return (
<React.Fragment>
{ child1 }
{ child2 }
</React.Fragment>
)
}
const App = () => {
const [a,setA] = useState(0)
const [b,setB] = useState(0)
return (
<div>
<Parent a={a} b=/>
<button onClick={() => setA(a + 1)}>改變a</button>
<button onClick={() => setB(b + 1)}>改變b</button>
</div>
)
}
ReactDOM.render(<App/>,document.getElementById('root'))
3.7. useRef
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
/**
* useRef: 保存引用值
* 兩個作用:
* 1. 相當(dāng)于類組件的一個實(shí)例屬性豌习,只要組件實(shí)例不銷毀存谎,就一直保持著引用,組件更新時也不會重新初始化,返回一個包含current屬性的對象
* 2. 獲取dom元素的一個引用
*
*/
const Counter1 = () => {
const [count,setCount] = useState(0)
// 初始化后會一直保持著引用肥隆,狀態(tài)變化組件更新重新渲染時也不會被重新初始化既荚,返回一個具有current屬性的對象
const countRef = useRef(0)
console.log('countRef',countRef)
useEffect(() => {
// 組件初始化和狀態(tài)更新時執(zhí)行
countRef.current = count
console.log('組件渲染完成')
})
const prevCount = countRef.current
return (
<div>
NowCount: {count}, beforeCount: {prevCount}
{ console.log('組件渲染中')}
<button onClick={() => setCount(count + 1)}>更新count</button>
</div>
)
}
// 使用類組件實(shí)現(xiàn)相似功能: 使用useRef定義的變量,相當(dāng)于類組件的實(shí)例屬性
class Counter2 extends React.Component {
state = { count: 0}
prevCount = 0 // 相當(dāng)于useRef定義的變量栋艳,不會在更新渲染時重新初始化
// 初始化渲染完成后執(zhí)行
componentDidMount() {
console.log('組件初始化渲染完畢Counter2')
}
// 數(shù)據(jù)狀態(tài)更新時執(zhí)行
componentDidUpdate() {
console.log('組件更新渲染完畢Counter2')
this.prevCount = this.state.count
}
render() {
return (
<div>
NowCount: {this.state.count}, beforeCount: {this.prevCount}
{ console.log('組件更新渲染中Counter2')}
<button onClick={() => this.setState({ count:this.state.count+1})}>更新count</button>
</div>
)
}
}
// 自定義屬性模擬類似功能
const countRef = { current: 0} // 唯一區(qū)別是因?yàn)槭褂玫娜肿兞壳∑福珻ounter3卸載時,該變量引用還在吸占,數(shù)據(jù)一直不會變晴叨,而使用useRef,組件卸載時引用會丟失
const Counter3 = () => {
const [count,setCount] = useState(0)
useEffect(() => {
console.log('組件初始化渲染/更新渲染完成Counter3')
countRef.current = count
})
const prevCount = countRef.current
return (
<div>
NowCount: {count}, beforeCount: { prevCount }
{ console.log('組件渲染中Counter3')}
<button onClick={() => setCount(count + 1)}>更新count</button>
</div>
)
}
// useRef第二個作用演示:獲取一個DOM元素的引用
const TextInputWithFocusButton = () => {
// 配合ref屬性使用可以獲取input元素的引用旬昭,類似react中的基礎(chǔ)api:React.createRef(); 唯一區(qū)別是篙螟,createRef在每次組件更新時都重新創(chuàng)建一個新的變量,useRef則一直會保持初始化時創(chuàng)建的對象的引用
const inputElement = useRef()
const onButtonOnFocus = () => {
console.log('inputElement',inputElement)
inputElement.current.focus()
}
return (
<React.Fragment>
<input ref={inputElement} type='text'/>
<button onClick={onButtonOnFocus}>Focus this input </button>
</React.Fragment>
)
}
const App = () => {
const [show,setShow] = useState(true)
return (
<div>
<h3>useRef第一個作用演示案例:</h3>
<div>
{ show ? <Counter1 /> : null}
<hr />
{ show ? <Counter2 /> : null}
<hr />
{ show ? <Counter3 /> : null}
<button onClick={() => setShow(!show)}>重新掛載</button>
</div>
<hr />
<h3>useRef第二個作用演示案例:</h3>
<div>
<TextInputWithFocusButton />
</div>
</div>
)
}
ReactDOM.render(<App/>,document.getElementById('root'))
3.8. useImperativehandle
- 先理解 forwardRef
import React, { createRef, forwardRef, useCallback } from "react";
import ReactDOM from "react-dom";
/**
* forwardRef:
* 是React的一個高級特性问拘,理解useRef之前需要先理解forwardRef
* 作用:forwardRef是一個高階組件,可以轉(zhuǎn)發(fā)收到的ref給其子組件,使其外部可以獲取對一個組件內(nèi)部子組件的引用
*/
// forwardRef是一個高階組件骤坐,它能將收到的ref轉(zhuǎn)發(fā)給它的子組件
const FancyButton = forwardRef((props,ref) => (
<div>
<input ref={ref}/>
<button>
{ props.children }
</button>
</div>
))
const App = () => {
// 創(chuàng)建一個ref引用
const ref = createRef()
/**
* ref本身是綁定到FancyButton上的引用绪杏,ref.current正常獲取到的應(yīng)該是FancyButton,但是在FancyButton內(nèi)部通過forwardRef就將引用轉(zhuǎn)發(fā)給了input纽绍,此時就將ref轉(zhuǎn)發(fā)到了input上蕾久,所以ref.current獲取到的就是input元素,因此才可以調(diào)用input元素的focus方法拌夏。
*/
const handleClick = useCallback(() => ref.current.focus(),[])
return (
<div>
{/* 將ref綁定引用到FancyButton上 */}
<FancyButton ref={ref}>點(diǎn)擊</FancyButton>
<button onClick={handleClick}>獲得焦點(diǎn)</button>
</div>
)
}
ReactDOM.render(<App/>,document.getElementById('root'))
- 搭配 forwardRef 和 useImperativeHandle 一起使用
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
import ReactDOM from "react-dom";
/**
* useImperativeHandle(): 透傳 Ref
* 作用:使父組件具備了獲取子組件【實(shí)例】和【狀態(tài)數(shù)據(jù)】的能力僧著,還可以根據(jù)參數(shù)來定義傳遞的數(shù)據(jù)是否要隨著子組件對應(yīng)數(shù)據(jù)的更新而更新
* 即:子組件通過useImperativeHandle自定義要傳遞給父組件的狀態(tài)或功能(想傳什么給父組件,就通過useImperativeHandle第二個參數(shù)返回值對象中定義即可)
* 注意:需要配合forwardRef一起使用障簿,需要用到forwardRef轉(zhuǎn)發(fā)ref給子組件的能力盹愚,否則無法獲取到對應(yīng)的ref引用,數(shù)據(jù)就不知道傳給誰了站故。
*/
/**
* 使用forwardRef皆怕,轉(zhuǎn)發(fā)FancyButton的ref引用到組件內(nèi)部
*/
const FancyButton = forwardRef((props,ref) => {
const inputRef = useRef()
const [inputValue,setInputValue] = useState(0)
// 在useImperativeHandle中自定義要返回的屬性給ref引用
useImperativeHandle(ref, () => ({
// 傳遞功能
focus: () => {
inputRef.current.focus()
},
// 傳遞狀態(tài)數(shù)據(jù)
inputValue,
// 傳遞實(shí)例屬性
inputRef
}))
return <input ref={inputRef} value={inputValue} onChange={(e) => setInputValue(e.target.value)}/>
})
const App = () => {
// 創(chuàng)建一個ref引用
const ref = useRef()
// 獲取input的焦點(diǎn)
const handleInputFocus = useCallback(() => {
ref.current.focus()
},[])
// 獲取傳遞的狀態(tài)數(shù)據(jù)
const handleGetChildState = useCallback(() => {
// 因此ref被轉(zhuǎn)發(fā)到了FancyButton內(nèi)部,經(jīng)過useImperativeHandle就可以獲取到返回的相關(guān)屬性
console.log('ref',ref.current)
// {inputValue: '11111', inputRef: {…}, focus: ?}
},[])
return (
<div>
{/* 綁定ref引用到 FancyButton上 */}
<FancyButton ref={ref}/>
<button onClick={handleInputFocus}>獲取子組件input的焦點(diǎn)</button>
<button onClick={handleGetChildState}>獲取子組件的狀態(tài)</button>
</div>
)
}
ReactDOM.render(<App />,document.getElementById('root'))
3.9. useLayoutEffect
import React, {useState,useLayoutEffect,useEffect} from "react";
import ReactDOM from "react-dom";
/**
* useLayOutEffect(): 同步執(zhí)行副作用
* 作用:
* 大部分情況下西篓,使用 useEffect 就可以幫我們處理組件的副作用愈腾,但是如果想要同步調(diào)用一些副作用,比如對 DOM 的操作岂津,就需要使用 useLayoutEffect虱黄,useLayoutEffect 中的副作用會在 DOM 更新之后同步執(zhí)行。與useEffect類似吮成,只是執(zhí)行時間不一樣,與類組件的componentDidMount 和 componentDidUpdate生命周期執(zhí)行時機(jī)一致
* 區(qū)別:
* 1. useLayoutEffect總是比useEffect先執(zhí)行
* 2. useEffect在全部渲染完畢后才會執(zhí)行(先渲染橱乱,后改變DOM),當(dāng)改變屏幕內(nèi)容時可能會產(chǎn)生閃爍
* 3. useLayoutEffect是會在瀏覽器 layout之后赁豆,painting 之前執(zhí)行(會推遲頁面顯示的事件仅醇,先改變DOM后渲染),不會產(chǎn)生閃爍
* 注意:
* 為了用戶體驗(yàn)魔种,優(yōu)先使用useEffect析二。以避免阻塞視圖更新,但如果涉及到同步調(diào)用一些副作用节预,比如操作dom叶摄,可以放在useLayoutEffect中
*/
function App() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
// 會在render,dom更新之后就執(zhí)行安拟,不會等到渲染完
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect"); // 先打印
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
useEffect(() => {
//dom渲染完畢后執(zhí)行
console.log("useEffect");
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'))
3.10. useDebugValue
import React, { useDebugValue, useState,useEffect } from "react";
import ReactDOM from "react-dom";
/**
* useDebugValue():
* 作用: 在自定義hooks中使用向開發(fā)者工具輸出一個調(diào)試值蛤吓,方便我們調(diào)試
*/
function useFriendStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const interval = setInterval(() => {
setIsOnline(isOnline => !isOnline);
}, 1000);
return () => clearInterval(interval);
}, []);
// 在React Developer Tools中hooks一欄顯示:
// e.g. "FriendStatus: Online"
useDebugValue(isOnline ? "Online" : "Offline");
return isOnline;
}
function App() {
const isOnline = useFriendStatus();
useDebugValue(isOnline ? "Online" : "Offline");
return <div className="App">用戶: {isOnline ? '在線' : '離線'}</div>;
}
ReactDOM.render(<App/>,document.getElementById('root') )
4. 自定義 Hooks
- React Hooks 中允許我們通過自定義 Hooks 實(shí)現(xiàn)公共邏輯的抽離,在不同組件之間復(fù)用糠赦。
- 自定義 Hooks 中可以使用官方提供的 Hooks 特性定義狀態(tài)數(shù)據(jù)和實(shí)現(xiàn)邏輯会傲,將邏輯封裝起來锅棕,通過 return 的方式返回外部需要的狀態(tài)和方法,不同的組件調(diào)用同一個 hook 只是復(fù)用了組件的邏輯淌山,并不會共享狀態(tài)裸燎。
- 自定義 Hooks 都以
use
開頭。
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
// 自定義hook
function useCount(){
// 公共邏輯放在內(nèi)部實(shí)現(xiàn)
let [count,setCount] = useState(0);
const setMyCount = () => {
setCount(count + 1)
}
// 只暴露外部需要的數(shù)據(jù)
return [count,setMyCount];
}
// 在不同組件中使用不會共享同一份數(shù)據(jù)泼疑,都是獨(dú)立的一份
function Example1(){
// setCount 為自定義hooks中返回的setMyCount
let [count,setCount] = useCount();
return (
<div>
Count: {count}
<button onClick={()=>{setCount()}}>更新count</button>
</div>
)
}
function Example2(){
let [count,setCount] = useCount();
return (
<div>
Count: {count}
<button onClick={()=>{ setCount()}}>更新count</button>
</div>
)
}
ReactDOM.render(<><Example1 /><Example2 /></>, document.getElementById('root'));
5. Hooks 參考周邊
本文首發(fā)于微信公眾號'前端螺絲釘'