1 關于hook
1.1 為什么使用hook
在react類組件(class)寫法中,有setState和生命周期對狀態進行管理,但是在函數組件中不存在這些,故引入hooks(版本:>=16.8),使開發者在非class的情況下使用更多react特性。
以下是實現一個輸入框,類組件和函數組件兩種寫法的對比:
/**
* @name 類組件
*/
import React from 'react';
export default class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'world'
};
}
componentDidMount() {
console.log('組件掛載后要做的操作')
}
componentWillUnmount() {
console.log('組件卸載要做的操作')
}
componentDidUpdate(prevProps, prevState) {
if (prevState.name !== this.state.name) {
console.log('組件更新后的操作')
}
}
render() {
return (
<div>
<p>hello {this.state.name}</p>
<input type="text" placeholder="input new name"
onChange={(e) => this.setState({ name: e.target.value })}>
</input>
</div>
);
}
}
/**
* @name 函數組件
*/
import React, { useState, useEffect } from 'react';
export default function Home() {
const [name, setName] = useState('world');
return (
<div>
<p>hello {name}</p>
<DemoState />
</div>
)
}
function DemoState() {
const [n1, setN1] = useState(1)
const [n2, setN2] = useState(2)
const [n3, setN3] = useState(3)
useEffect(() => {
setN1(10)
setN1(100)
}, [])
const handleClick = () => {
setN2(20)
setN3(30)
}
console.log('demo-state', n1, n2, n3)
return <button onClick={handleClick}>click</button>
}
上述例子中,useState相當于constructor,完成數據的初始化;
useEffect相當于componentDidMount和componentDidUpdate兩個生命周期,通過return () => {}的方式解綁生命周期,相當于componentWillUnmount周期,以監聽頁面滾動為例,通過effect實現監聽與解綁如下:
useEffect(() = >{
window.addEventListener(‘scroll’, throttleFunc)
return () = >{
window.removeEventListener(‘scroll’, throttleFunc)
}
}, [])
在同一個effect鉤子中實現綁定與解綁,使狀態的管理更加方便、代碼更簡潔。
此外還有發生在頁面渲染前的useMemo相當于shouldComponentUpdate周期等,具體關系如下表:
class組件 | hooks |
---|---|
shouldComponentUpdate | useMemo |
render | 函數本身 |
getDerivedStateFromProps | useState 中的 update |
getDerivedStateFromError | 無 |
constructor | useState |
componentWillUnmount | useEffect中的return函數 |
componentDidUpdate | useEffect |
componentDidMount | useEffect |
componentDidCatch | 無 |
結論:使用hooks的函數組件,簡化了很多代碼,不用維護復雜的生命周期,也不用擔心this的指向問題。
1.2 什么是hook
hooks掛載在Fiber結點上的memoizedState,filber結構如下:
FiberNode { // fiber結構
memoziedState, // 組件更新的依據
type, // 原生或react
key,
tag,
...
}
memoziedState這個字段很重要,是組件更新的唯一依據。在class組件里,它就是this.state的結構,調用this.setState的時候,其實就是修改了它的數據,數據改變了組件就會重新執行。
也就是說,即使是class組件,也不會主動調用任何生命周期函數,而是在memoziedState改變后,組件重新執行,在執行的過程中才會經過這些周期。
所以,這就解釋了函數式組件為什么可以通過hooks改變狀態,實際上就是修改了對應fiber節點的memoziedState。
hooks主要有以下特點:
1、無需修改組件結構的情況下復用狀態邏輯;
2、可將組件中相互關聯的部分拆分成更小的函數,復雜組件將變得更容易理解;
3、每一個組件內的函數(包括事件處理函數,effects,定時器或者api調用等等)會捕獲某次渲染中定義的props和state;
4、memo緩存組件 ,useMemo緩存值, useCallback緩存函數;
5、每次render都有自己的props、state和effects。(每一個組件內的函數,包括事件處理函數,effects,定時器或者api調用等等,會捕獲某次渲染中定義的props和state);
6、更新狀態的時候(如setCount(count + 1)),React會重新渲染組件,每一次渲染都能拿到獨立的count狀態,這個狀態值是函數中的一個常量;
7、沒有了顯性的生命周期,所有渲染后的執行方法都在useEffect里面統一管理;
8、函數式編程,不需要定義constructor、render、class;
9、某一個組件,方法需不需要渲染、重新執行完全取決于開發者,方便管理。
1.3 常見hook
useState、useEffect、useMemo、useCallback、useRef、useContext、useReducer…。
所有的鉤子都是為函數引入外部功能,react約定,鉤子一律使用use前綴命名。
2 常用hook
2.1 useState
示例:
const [stateA, setStateA] = useState(0)
參數是初始state(定義初始state最好給出初始值,方便后期維護, 0/false/’’/[]/{})。
返回值:一個是當前state,一個是更新state的函數。
useState的實現很簡單,只有兩行
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
重點都在dispatcher上,dispatcher通過resolveDispatcher()來獲取,這個函數只是將ReactCurrentDispatcher.current的值賦給了dispatcher
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
useState掛在dispatcher上,resolveDispatcher() 返回的是 ReactCurrentDispatcher.current,所以useState(xxx)等價于ReactCurrentDispatcher.current.useState(xxx)。
useState(hooks)的具體執行過程如下:
- updateContainer → … → beginWork
- beginWork中會根據當前要執行更新的fiber的tag來判斷執行什么,在函數式組件,執行了updateFunctionComponent(判斷執行函數式/組件式更新)
首次渲染時,React Fiber 會從 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork() 開始執行。在beginWork函數中,可以根據workInProgress(是一個Fiber節點)上的tag值來走不通的方法加載或更新組件,如下:
function beginWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) : Fiber | null {
/****/
// 根據不同的組件類型走不同的方法
switch (workInProgress.tag) {
// 不確定組件
case IndeterminateComponent:
{
const elementType = workInProgress.elementType;
// 加載初始組件
return mountIndeterminateComponent(current, workInProgress, elementType, renderExpirationTime, );
}
// 函數組件
case FunctionComponent:
{
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = workInProgress.elementType === Component ? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);
// 更新函數組件
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderExpirationTime, );
}
// 類組件
case ClassComponent:
{
/****/
}
}
}
- 在updateFunctionComponent中,對hooks的處理如下
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
所以,React Hooks 的渲染核心是renderWithHooks,在renderWithHooks函數中,初始化了Dispatcher。
export
function renderWithHooks < Props, SecondArg > (current: Fiber | null, workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) = >any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ) : any {
// 若Fiber為空,則認為是首次加載
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 掛載時的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useMemo: mountMemo,
useState: mountState,
// ...
};
// 更新時的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
// ...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useRef: updateRef,
useState: updateState,
// ....
};
}
- 在renderWithHooks中,會先根據fiber的memoizedState是否為null,來判斷是否已經初始化。因為memoizedState在函數式組件中是存放hooks的。是則mount,否則update(判斷是否執行過,沒有則掛載,有則更新)
- 在mount(掛載)時,函數式組件執行,ReactCurrentDispatcher.current為HooksDispatcherOnMount,被調用,會初始化hooks鏈表、initialState、dispatch函數,并返回。這里就完成了useState的初始化,后續函數式組件繼續執行,完成渲染返回。(首次渲染過程)
- 在update(更新)時,函數式組件執行,ReactCurrentDispatcher.current為HooksDispatcherOnUpdate,被調用,updateWorkInProgressHook用于獲取當前work的Hook。然后根據numberOfReRenders 是否大于0來判斷是否處理re-render狀態:是的話,執行renderPhaseUpdates,獲取第一個update,然后循環執行,獲取新的state,直到下一個update為null;否的話,獲取update鏈表的第一個update,進行循環,判斷update的優先級是否需要更新,對于優先級高的進行更新。(更新過程)
- 結果返回當前狀態和修改狀態的方法
以掛載為例,生成一個hook對象(mountState),并對hook對象進行初始化(mountWorkInProgressHook),具體如下:
function mountState < S > (initialState: (() = >S) | S, ) : [S, Dispatch < BasicStateAction < S >> ] {
// 創建一個新的hook對象,并返回當前workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 第二步:獲取初始值并初始化hook對象
const queue = hook.queue = { // 新建一個隊列
// 保存 update 對象
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 一次新的dispatch觸發前最新的reducer
// useState 保存固定函數: 可以理解為一個react 內置的reducer
// (state, action) => { return typeof action === 'function' ? action(state) : action }
lastRenderedReducer: reducer
// 一次新的dispatch觸發前最新的state
lastRenderedState: (initialState: any),
}
// 綁定當前 fiber 和 queue.
const dispatch: Dispatch < BasicStateAction < S > ,
>=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
// 返回當前狀態和修改狀態的方法
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook() {
// 初始化的hook對象
var hook = {
memoizedState: null,
// 存儲更新后的state值
baseState: null,
// 存儲更新前的state
baseQueue, // 更新函數
queue: null,
// 存儲多次的更新行為
next: null // 指向下一次useState的hook對象
};
// workInProgressHook是一個全局變量,表示當前正在處理的hook
// 如果workInProgressHook鏈表為null就將新建的hook對象賦值給它,如果不為null,那么就加在鏈表尾部。
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
初始化完成后,setFn又是怎么對stateA值進行更新的呢?實際上就是通過dispatchAction方法進行更新的,如下:
// currentlyRenderingFiber$1是一個全局變量,表示當前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
此外還有對hook對象的更新(dispatchAction),如下:
function dispatchAction(fiber, queue, action) {
// ...
// 1. 創建update對象
// 該對象保存的是調度優先級/state/reducer以及用戶調用dispatch/setState 時傳入的action
const update: Update < S,
A > ={
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 2. 將update更新到queue.pending中,最后的update.next 指向第一個update對象,形成一個閉環。
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
簡單理解:
- 初次渲染的時候,按照 useState,useEffect 的順序,把 state,deps 等按順序塞到 memoizedState 數組中,共享同一個 memoizedState,共享同一個順序。
- 更新的時候,按照順序,從 memoizedState 中把上次記錄的值拿出來。
let memoizedState = []; // hooks 存放在這個數組
let cursor = 0; // 當前 memoizedState 下標
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor] || initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState]; // 返回當前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
- 具體 看下圖
1、初始化時:數組為空,下標置0
2、首次渲染:將遇到的hook的依賴項加入數組,與下標一一對應
3、事件觸發:觸發hook的內容被修改,修改后的數據替換掉數組中原先的數據
4、重渲染:ReRender 的時候,重新去執行函數組件,但是對之前已經執行過的函數組件并不會做任何操作
下面看一個實例:
const DemoState = memo(() => {
const [n1, setN1] = useState(1)
const [n2, setN2] = useState(2)
const [n3, setN3] = useState(3)
useEffect(() => {
setN1(10)
setN1(100)
}, [])
const handleClick = () => {
setN2(20)
setN3(30)
}
console.log('demo-state', n1, n2, n3)
return <div>
<div className={`${classPrefix}-title`}>---useState---</div>
<button onClick={handleClick}>改變n2、n3</button>
</div>
})
// demo-state 1 2 3 => demo-state 100 2 3 => demo-state 100 20 30
渲染時,effect對n1進行了兩次賦值,實際上僅刷新一次;點擊事件分別對n1和n2進行賦值,實際上也僅刷新一次。
結論:setState返回的函數執行會導致re-render;
框架內部會對多次函數操作進行合并,保證useState拿到最新的狀態,避免重復渲染。
如果初始state需要通過復雜計算獲得,可以傳入一個函數,在函數中計算并返回初始state,此函數只在初始渲染時被調用,具體如下:
const [count, setCount] = useState(() => {
const initialCount = someExpensiveComputation(props)
return initialState
})
那么,state在class和function中有什么區別嗎?下面兩段代碼中,1s內點擊事件各觸發5次會有什么表現嗎?
class HooksDemoRule extends React.PureComponent {
state = {
count: 0
}
increment = () => {
console.log('---setState---')
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
}, 3000)
}
render() {
return <div>
<div className={`${classPrefix}-title`}>---useState && setState---</div>
<div className={`${classPrefix}-text`}>setState 當前值:{this.state.count}</div>
<button onClick={this.increment}>+1</button>
<IKWebviewRouterLink
ikTo='home'
className={`${classPrefix}-to-home`}
><p>返回</p></IKWebviewRouterLink>
</div>
}
}
const SecondDemo = memo(() => {
const [count, setCount] = useState(0)
const increment = () => {
console.log('---useState---')
setTimeout(() => {
setCount(count + 1)
}, 3000)
}
return <div>
<div className={`${classPrefix}-title`}>---useState && setState---</div>
<div className={`${classPrefix}-text`}>useState 當前值:{count}</div>
<button onClick={increment}>+1</button>
<IKWebviewRouterLink
ikTo='rule'
className={`${classPrefix}-to-home`}
><p>返回</p></IKWebviewRouterLink>
</div>
})
最終結果:在類組件中,頁面上的數字依次從0增加到5;在函數組件中,頁面上的數字只會從0增加到1。
原因在于,在類組件中,通過this.state引用count,每一次setTimeout的時候都能通過引用拿到上一次的最新count,所以最后加到5。但是在函數組件中,每一次更新都是重新執行當前函數,1s內setTimeout里讀取的count實際上都是初始值0,所以最后只加到1。如果想讓函數組件也加到5要怎么實現呢,下文useRef會講到。
簡單來說,類組件的state依賴上一次state,函數組件的state是重新執行當前函數。
2.2 useEffect
useEffect的實現很簡單,也是只有兩行:
export function useEffect (create: () = >(() = >void) | void, deps: Array < mixed > |void | null, ) : void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
useEffect產生的hook會放在fiber.memoizedState上,調用后生成一個effect對象,存儲到對應hook的memoizedState中,與其他effect連接成環形鏈表。
單個的effect對象包含以下幾個屬性:
create:傳入useEffect函數的第一個參數,即回調函數;
destory:回調函數中的return函數,在改effect銷毀的時候執行,默認發生在第一次渲染后,也可以讓它在依賴項數組中的值改變時執行,通過return清除副作用函數(如監聽、訂閱、計時器等);
deps:依賴項,傳入的第二個參數,用來控制該Effect包裹的函數執不執行。如果依賴項為空數組[],則該Effect在每次組件掛載時執行,且僅執行一次,相當于class組件中的componentDidMount和componentDidupdate生命周期的融合;如果沒有第二個參數,則effect會不停地調用。
next:指向下一個effect;
tag:effect的類型,區分useEffect和useLayoputEffect。
hook會掛載到fiber.memoizedState上。hook按出現順序進行存儲,memoizedState存儲了useEffect的effect對象(effect1),next指向useLayoutEffect的effect對象(effect2),effect2的next又會指向effect1,最終形成閉環。結構如下:
const DemoEffect = memo(() => {
useEffect(() => {
console.log('useEffect1');
const timeId = setTimeout(() => {
console.log('useEffect1-setTimeout-2000');
}, 2000);
return () => {
clearTimeout(timeId);
};
}, []);
useEffect(() => {
console.log('useEffect2');
const timeId = setInterval(() => {
console.log('useEffect2-setInterval-1000');
}, 1000);
return () => {
clearInterval(timeId);
};
}, []);
return (
<div>
<div className={`${classPrefix}-title`}>---useEffect---</div>
{(() => {
console.log('render');
return null;
})()}
</div>
);
})
// render => useEffect1 => useEffect2 => useEffect2-setInterval-1000 => useEffect1-setTimeout-2000 => index.js:67 useEffect2-setInterval-1000 * n
結論:effect在頁面完成渲染后按照先后順序執行,并且內部執行時異步的
useEffect和useLayoutEffect:
useLayoutEffect也是一個hook方法,跟useEffect類似,區別在于渲染時機不同,useEffect發生在瀏覽器渲染結束后執行,useLayoutEffect則是發生在dom更新完成后。
PS:useEffect和useLayoutEffect都是effect鉤子
下面是一個方塊移動的例子,在effect中添加右移的方法,理解兩者的區別:兩者都發生在render之后,且useLayoutEffect發生在useEffect之前
const moveTo = (dom, delay, options) => {
dom.style.transform = `translate(${options.x}px)`
dom.style.transition = `left ${delay}ms`
}
const Animate = memo(() => {
const squRef = useRef()
const squRef1 = useRef()
const squRef2 = useRef()
useLayoutEffect(() => { // 方塊直接出現在右側,不會閃一下
console.log('useLayoutEffect-1')
moveTo(squRef1.current, 500, { x: 600 })
}, [])
useEffect(() => { // 會有方塊移動的過程,閃一下
console.log('useEffect')
moveTo(squRef.current, 500, { x: 600 })
}, [])
useLayoutEffect(() => {
console.log('useLayoutEffect-2')
moveTo(squRef2.current, 500, { x: 600 })
}, [])
console.log('render')
return (
<>
<div className={`${classPrefix}-title`}>---useEffect && useLayoutEffect---</div>
<div className={`${classPrefix}-square`} ref={squRef}></div>
<div className={`${classPrefix}-square1`} ref={squRef1}></div>
<div className={`${classPrefix}-square1`} ref={squRef2}></div>
</>
)
})
// render -> useLayoutEffect-1 -> useLayoutEffect-2 -> useEffect
useLayoutEffect和useEffect很像,唯一的不同點就是useEffect是異步執行,而useLayoutEffect是同步執行的。
當函數組件刷新(渲染)時,
包含useEffect的組件整個運行過程如下:
1、觸發組件重新渲染(通過改變組件state或者組件的父組件重新渲染,導致子節點渲染)
2、組件函數執行
3、組件渲染后呈現到屏幕上
4、useEffect hook執行
包含useLayoutEffect的組件整個運行過程如下:
1、觸發組件重新渲染(通過改變組件state或者組件的父組件重新渲染,導致子組件渲染)
2、組件函數執行
3、useLayoutEffect hook執行, React等待useLayoutEffect的函數執行完畢
4、組件渲染后呈現到屏幕上
useEffect異步執行的優點是,react渲染組件不必等待useEffect函數執行完畢,造成阻塞。
百分之99的情況,使用useEffect就可以了,唯一需要用到useLayoutEffect的情況就是,在使用useEffect的情況下,我們的屏幕會出現閃爍的情況(組件在很短的時間內渲染了兩次)。
2.3 useReducer
參數:第一個是reducer純函數,第二個是初始state,第三個是修改初始state,用于重置
返回值是一個數組,數組第一個元素是state的當前值,第二個元素是發送action的dispatch函數
管理包含多個子值的state對象時,應該怎么處理呢?以獲取某一接口為例,具體操作如下:
const fetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT': // 接口初始化
return {
...state,
status: '初始化',
loading: true,
error: false
};
case 'FETCH_SUCCESS': // 請求成功
return {
...state,
status: '成功',
loading: false,
error: false,
data: action.data
};
case 'FETCH_FAIL': // 請求失敗
return {
...state,
status: '失敗',
loading: false,
error: true
};
default:
return null
}
};
const DemoReducer = memo(() => {
const [state, dispatch] = useReducer(fetchReducer, {
loading: false,
error: false,
status: '',
data: {}
});
const getData = async () => {
const { data } = await Apis.GET_USER_INFO().catch(() => {
dispatch({ type: 'FETCH_FAIL', data: null })
return false
})
if (!data) return
dispatch({ type: 'FETCH_SUCCESS', data })
}
useEffect(() => {
dispatch({ type: 'FETCH_INIT' })
getData()
}, [])
console.log('state---', state)
return <div>
<div className={`${classPrefix}-title`}>---useReducer---</div>
<div className={`${classPrefix}-text`}>請求狀態: {state.status}</div>
</div>
})
結論:useReducer可以處理多個用useState實現的邏輯(加載狀態、錯誤信息、請求數據)
思考題:useState的出現,讓我們可以使用多個state變量來保存state,如
const [width, setWidth] = useState(100)
const [height, setHeight] = useState(100)
const [left, setPageX] = useState(0)
const [top, setPageY] = useState(0)
也可以像class組件的this.state一樣,將所有state放在一個obj中,如
const [state, setState] = useState({ width: 100, height: 100, left: 0, top: 0 });
也可以使用useReducer處理,如
const stateReducer = (state, action) => {
switch (action.type) {
case 'WIDTH':
return {
...state,
width: action.width
};
case 'HEIGHT':
return {
...state,
height: action.height
};
......
}
const [state, dispatch] = useReducer(stateReducer, { width: 100, height: 100, left: 0, top: 0 })
這三種方法哪種好呢?
2.4 useMemo
參數是創建函數和依賴項數組。
返回值是一個帶有memoized的值,發生在render之前, 并且這個值僅在依賴項改變時才重新計算。
const DemoMemo = memo(() => {
const [num1, setNum1] = useState(1)
const [num2, setNum2] = useState(2)
const expensive = useMemo(() => {
console.log('運算')
let sum = 0
for (let i = 0; i < num1 * 100; i++) {
sum += i
}
return sum
}, [num1])
const handleClick1 = () => {
console.log('num1++')
setNum1(num1 + 1)
}
const handleClick2 = () => {
console.log('num2++')
setNum2(num2 + 1)
}
return <div>
<div className={`${classPrefix}-title`}>---useMemo---</div>
<div className={`${classPrefix}-text`}>當前num1:{num1}</div>
<div className={`${classPrefix}-text`}>當前num2:{num2}</div>
<div className={`${classPrefix}-text`}>當前expensive(僅依賴num1):{expensive}</div>
<div>
{(() => {
console.log('render');
return null;
})()}
<button onClick={handleClick1}>num1++</button>
<button onClick={handleClick2}>num2++</button>
</div>
</div>
})
// 運算 => render
// 點擊num1++: num1++ => 運算 => render
// 點擊num2++: num1++ => render
結論:useMemo發生在render前,返回一個緩存的數據,且僅在依賴項改變后變化。
使用useMemo可以避免多余的計算開銷。
2.5 useCallback
參數是內聯回調函數和依賴項數組,
返回值是回調函數的memoized版,該回調函數僅在某個依賴項改變時才會更新。
const set = new Set();
const DemoCallback = memo(() => {
const [num1, setNum1] = useState(1)
const [num2, setNum2] = useState(2)
const callback = useCallback(() => {
// 這里做復雜運算
return num1;
}, [num1])
set.add(callback)
const handleClick1 = () => {
setNum1(num1 + 1)
}
const handleClick2 = () => {
setNum2(num2 + 1)
}
console.log('demo-callback', set.size)
return <div>
<div className={`${classPrefix}-title`}>---useCallback---</div>
<div className={`${classPrefix}-text`}>當前num1:{num1}</div>
<Child callback={callback}/>
<div>
<button onClick={handleClick1}>num1++</button>
<button onClick={handleClick2}>num2++</button>
</div>
</div>
})
const Child = memo(({ callback }) => {
console.log('---child render')
return <div>
<div className={`${classPrefix}-text`}>child刷新(僅依賴num1):{set.size}</div>
</div>
})
// demo-callback 1 => ---child render
// 點擊num1++: demo-callback 2 => ---child render
// 點擊num2++: demo-callback 2
結論:返回一個緩存的函數,添加依賴項數組可以避免函數的無意義計算,降低了子組件的渲染開銷。
2.6 useRef
返回值是一個可變的ref對象,并且這個對象的值發生改變時不會引起頁面的渲染。
const DemoRef = memo(() => {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus()
inputEl.current.value = '自定義'
};
return (
<>
<div className={`${classPrefix}-title`}>---useRef---</div>
{(() => {
console.log('render');
return null;
})()}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>click</button>
</>
);
})
// render
// 點擊click:
結論:useRef可以存儲不需要引起頁面渲染的數據;修改useRef值的唯一方法是修改.current,且修改后不會引起重渲染。
2.1中的問題,可以通過以下方法解決
const SecondDemoNew = memo(() => {
const [ count, setCount ] = useState(0)
const ref = useRef(0)
const increment = () => {
console.log('---useState---')
setTimeout(() => {
setCount((ref.current += 1))
// setCount(count => count + 1)
}, 3000)
}
return <div>
<div className={`${classPrefix}-title`}>---useState && setState---</div>
<div className={`${classPrefix}-text`}>當前值:{count}</div>
<button onClick={increment}>+1</button>
</div>
})
另外, useRef也可以用來實現錨點跳轉,具體如下
const scrollRef = useRef(null)
const scrollToRef = (ref) = >{ // 跳轉
if (!ref) return
ref.current.scrollIntoView({
behavior: 'smooth'
})
}
......
< div onClick = { () = >scrollToRef(scrollRef) } > 航行進度 < /div>
<span ref={scrollRef}></span >
2.7 useContext
跨組件共享數據的鉤子函數,接收一個context對象,并返回該對象的當前值。
當前的context值由上層組件中距離當前組件最近的<MyContext.Provider>的value決定,并且父組件的context發生改變是,子組件都會重新渲染。
const MyContext = React.createContext() // 創建context,用于支持調用
const DemoContext = memo(() => {
const [value, setValue] = useState('initValue')
return (
<div>
<div className={`${classPrefix}-title`}>---useContext---</div>
{(() => {
console.log('render');
return null;
})()}
<button onClick={() => {
setValue('newValue')
}}>
改變value
</button>
<MyContext.Provider value={value}>
<Child1 />
<Child2 />
</MyContext.Provider>
</div>
);
})
const Child1 = memo(() => {
const value = useContext(MyContext)
console.log('Child1-value', value)
return <div className={`${classPrefix}-text`}>Child1-value: {value}</div>
})
const Child2 = () => {
console.log('Child2');
return <div className={`${classPrefix}-text`}>Child2</div>;
}
// render => Child1-value initValue => Child2
// 點擊btn后: render => Child1-value newValue => Child2
結論:useContext會在context值變化時重新渲染,<MyContext.Provider>的value發生變化時,包裹的組件無論是否訂閱value值,都會重新渲染,可以使用memo對未使用value的子組件進行優化。
2.8 自定義hook
有時候我們需要重復使用一些狀態邏輯,怎么處理可以在不增加組件的前提下實現復用,自定義hook可以達到這一目的。
通過自定義hook,抽取多個組件重復使用的邏輯,將這些重復的邏輯添加到一個叫做useSomething的自定義hook中,調用這一hook達到邏輯復用的目的,在不增加組件的情況下實現了邏輯共享。
自定義的hook是一個函數,名稱以“use”開頭,函數內部可用調用其他hook。以處理請求過程為例,自定義hook如下:
3 優化
hook主要從以下三個方面對函數式組件進行優化:
useCallback用于緩存函數
useMemo用于緩存計算結果,簡單理解useCallback(fn, deps) === useMemo(() => fn, deps)
useReducer用于處理多狀態的state
4 小結
hooks執行流程:
在react中,組件返回的JSX元素被轉換為虛擬DOM,就是下方的vnode,每個vnode上掛載了一個_component屬性,這個屬性指向組件實例。在組件實例上又掛載了一個_hooks屬性,這個_hooks屬性里保存了執行一個組件時,里面所有Hook方法的相關信息。
首先,有一個全局的currentIndex變量,當組件第一次渲染或更新時,它會在每次進入一個函數組件的時候都重置為0,每次遇到一個hook方法就會加1,同時將這個hook方法加到_list(緩存)中。當下次進來或進入下一組件時,currentIndex又被重置為0;在組件更新時,則會從_list中根據currentIndex取出對應項。具體如下添加鏈接描述:
組件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放進 _list數組 => 索引currentIndex++ => 渲染結束
組件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,獲取 _list[currentIndex]=> currentIndex++ => 重復上面步驟 => 更新結束
hooks使用規則:(eslint-plugin-react-hooks控制以下兩條規則)
- 只能在函數最外層調用 Hook。不要在循環、條件判斷或者子函數中調用(hook2拿到的state其實是上一次hook1執行后的state, 而不是上一次hook2執行后的state。如果把hook1放在一個if語句中,當這個hook沒有執行時,這樣顯然會發生錯誤)。
- 只能在 React 的函數組件中調用 Hook(因為只有函數組件的更新才會觸發renderWithHooks函數,處理hooks的相關邏輯)。
hooks優化策略:
優化本身也會帶來大量的計算,無意義的優化反而會增加額外的開銷。所以針對3中優化需謹慎。
https://blog.csdn.net/qq_30997503/article/details/117228632?spm=1001.2014.3001.5502
https://github.com/brickspert/blog/issues/26