在組件之間複用狀態邏輯很難, providers,consumers,高階組件,render props等能夠將橫切關注點 (如校驗,日誌,異常等) 與核心業務邏輯分離開,可是使用過程當中也會帶來擴展性限制,ref傳值問題,「嵌套地獄」等問題;Hook 提供一種簡單直接的代碼複用方式,可使開發者在無需修改組件結構的狀況下複用狀態邏輯。html
複雜組件生命週期經常包含一些不相關的邏輯,相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒;不少開發者將 React 與狀態管理庫結合使用,這每每會引入了不少抽象概念,開發過程當中還須要在不一樣的文件之間來回切換;Hook 提供一種更合理的代碼組織方式,能夠將組件中相互關聯的代碼彙集在一塊兒,而不是被生命週期方法強制拆開,使其更加可預測。前端
class 組件存在着一些問題(如:class 不利於代碼壓縮,而且會使熱重載出現不穩定的狀況);Hook支持函數組件,使開發者在非class 的狀況下可使用更多的 React 特性。react
Hooks 只能在函數組件 (FunctionComponent) 中使用,賦予無實例無生命週期的函數組件以 class 組件的表達力而且更合理地拆分/組織代碼,解決複用問題。git
Fiber 提供了 hooks 實現的基礎:hooks 是基於 Fiber 對象上能存儲 memoizedState,它以雙向鏈表的形式存儲在 Fiber 的 memoizedState 字段中。github
Fiber 把應用樹區分紅每個節點的更新,每個 ReactElement 對應一個 Fiber 對象,Fiber 呈鏈表結構,串聯整個應用樹結構 (child, siblings, return),如圖:數組
Fiber 會記錄節點的各類狀態 (state, props)(包括functional Component),而且在 update 的時候,會從原來的 Fiber(current)clone 出一個新的 Fiber(alternate)。兩個 Fiber diff 出的變化(side effect)記錄在 alternate上,在更新結束後 alternate 會取代以前的 current 的成爲新的 current 節點。瀏覽器
Fiber 對 react 渲染機制的改變主要的影響:微信
異步更新:由於 Fiber 把應用樹區來分紅每個節點的更新,它們的更新互相獨立,不會有相互的影響,因此能夠異步打斷如今的更新,而後去等待一個別的任務執行完成以後回過頭來繼續進行更新。數據結構
提供了 hooks 實現的基礎:hooks 是基於 Fiber 對象上能存儲 memoizedState, 基於 memoizedState 上能夠存儲這些東西,一步一步向下構建了 hooks API 的體系。dom
經常使用的:useState, useEffect, useContext, useReducer;
此外不經常使用的:useLayoutEffect, useCallback, useMemo, useRef, useImperativeHandle。
以 useState 爲例瞭解 hook 的渲染更新過程 先了解 Hook 的數據結構
export type Hook = {
memoizedState: any, //上一次渲染的時候的state
baseState: any, // 當前正在處理的state
baseUpdate: Update<any, any> | null, // 當前的更新
queue: UpdateQueue<any, any> | null, // 產生的update放在這個隊列裏
next: Hook | null, // 下一個
};
複製代碼
運行下面的組件代碼
export default function App() {
const [name, setName] = useState('dora')
const nameChange = e => {
setName(e.target.value)
}
return (
<React.Fragment>
<input type='text' value={name} onChange={nameChange} />
<p>{name}</p>
</React.Fragment>
)
}
複製代碼
初始化 state 時調用 mountState,初始化 initialState,而且記錄在 workInProgressHook.memoizedState 和 workInProgressHook.baseState上,而後建立 queue 對象, queue 的 dispatch 屬性是用來記錄更新 state 的方法的,dispatch 就是 dispatchAction綁定了對應的 Fiber 和 queue。而後返回初始的格式 [name, setName] = useState('dora');執行源碼以下:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
複製代碼
再來看更新過程
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
複製代碼
因而可知 useState 只是個語法糖,本質就是 useReducer;那麼再來看 useReducer:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
if (numberOfReRenders > 0) {
//在當前更新週期中又產生了新的更新
//就繼續執行這些更新直到當前渲染週期中沒有更新爲止
...
}
const last = queue.last;
const baseUpdate = hook.baseUpdate;
const baseState = hook.baseState;
let first;
if (baseUpdate !== null) {
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
}
} else {
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
} const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
複製代碼
就是根據 reducer 和 update.action 來建立新的 state,並賦值給 Hook.memoizedState以及 Hook.baseState;在當前更新週期中又產生了新的更新, 就繼續執行這些更新直到當前渲染週期中沒有更新爲止。而後對每一個更新判斷其優先級(根據expirationTime值的大小),若是不是當前總體更新優先級內得更新會跳過,第一個跳過得 Update 會變成新的 baseUpdate,他記錄了在以後全部得 Update,即使是優先級比他高得,由於在他被執行得時候,須要保證後續的更新要在他更新以後的基礎上再次執行。
最後執行 dispatchAction方法,發起一次 scheduleWork 的調度,完成更新,此處省略代碼。
useEffect 和 useLayoutEffect 帶給 FunctionalComponent 產生反作用能力的 Hooks,他們的行爲很是相似 componentDidMount 和 componentDidUpdate 的合集,而且經過 return 一個函數指定如何「清除」反作用。
先看 useEffect 和 useLayoutEffect 更新的過程:
二者都調用了 updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) 方法,傳入的第二個參數又做爲 pushEffect 的入參生成一個新的 effect。
這個 effect 的 tag 就是入參 hookEffectTag,pushEffect 方法返回一個新的 effect,而且建立了一個 updateQueue,這個 queue 會在 commit 階段被執行。
能夠看到,這個階段,useEffect 和 useLayoutEffect 的主要區別在生成的 effect 的 tag參數不一樣,經過計算,二者的tag值分別爲二進制:0b11000000 和 0b00100100;
再來看 commit 階段調用的 commitHookEffectList 方法。
經過對比傳入的 effectTag(unmountTag & mountTag) 和 Hook 對象上的 effectTag,判斷是否須要執行對應的 destory 和 create 方法,那麼又在哪些地方調用了commitHookEffectList 方法呢?能夠看下其中兩處:commitLifeCycles 和 commitWork。
在 commitLifeCycles 中傳入的 unmountTag 和 mountTag 值分別爲:0b00010000 和 0b00100000; 在 commitWork 中傳入的 unmountTag 和 mountTag 值分別爲:0b00000100 和 0b00001000; 分別計算 effect.tag & unmountTag 和 effect.tag & mountTag:
能夠看到 useLayoutEffect 的 destory 會在 commitWork 的時候被執行;而他的 create會在 commitLifeCycles 的時候被執行;useEffect 在這個流程中都不會被執行。
事實上:
useLayoutEffect 會在當前 commit 執行的過程當中就會被執行 destroy 和 create, 而對於 useEffect,會異步地等到此次全部的 dom 節點更新完成,瀏覽器渲染完成後,纔會去執行這部分代碼。
它對於 useLayoutEffect 來講,它是不會去阻塞瀏覽器的渲染,由於咱們可能在 useLayoutEffect 裏面去執行一些 dom 相關的操做,甚至 setState 來執行一些更新,這種更新都會同步執行,至關於 react 的運行時它要佔用更長的 js 的運行時間,致使瀏覽器沒有時間去渲染,最終可能會致使頁面會有些卡頓。
服務端渲染狀況下,不管 useLayoutEffect 仍是 useEffect 都沒法在 Javascript 代碼加載完成以前執行。能夠經過使用 showChild &&進行條件渲染,並使用 useEffect(() => { setShowChild(true); }, []) 延遲展現組件。
useLayoutEffect 的執行過程跟 componentDidMount 和 componentDidUpdate 很是類似,因此 React 官方也說了,若是你必定要選擇一個相似於生命週期方法的 Hook,那麼 useLayoutEffect 是不會錯的那個,可是咱們推薦你使用 useEffect,在你清楚他們的區別的前提下,後者是更好的選擇。
更多 Hook 使用,請查看如下文檔
官方文檔 zh-hans.reactjs.org/docs/hooks-…
做者:朵拉
掃碼關注:「銅板街科技」微信公衆號,精彩內容按期推送。公衆號消息界面回覆「推薦」「前端」「Java」「客戶端」獲取更多精準內容。