前言
Preact 是什麼?React 的 3kb 輕量化方案,擁有同樣的 ES6API
雖然 Preact 和 React 有着相同的 API, 但是其內部實現機制的差異依然是巨大。但是這並不妨礙我們閱讀以及學習 Preact 的源碼。說一句題外話,今年年初的時候,我的一位哥們@小寒,在北京某家公司面試時遇到了來自 Facebook 的大牛,這位 Facebook 的大牛也曾推薦過他,閱讀學習 Preact 的源碼。
hooks 不是什麼魔法,hooks 的設計也與 React 無關(Dan Abramov)。在 Preact 中也是如此,所以即使你沒有閱讀過 Preact 或者 React 源碼,也不妨礙理解 hooks 的實現。
希望下面的分享,對大家理解 hooks 背後的實現能有所啓示。
關於 hooks 的規則
React 中 hooks 的使用規則如下。我們可以看出 hooks 的使用,高度的依賴執行順序。在閱讀完源碼後,我們就會知道,爲什麼 hooks 的使用會有這兩條規則。
- ✅ 只在最頂層使用 hook。不要在循環,條件或嵌套函數中調用 hook。
- ✅ 不要在普通的 JavaScript 函數中調用 Hook。
hooks 源碼解析
getHookState
getHookState
函數,會在當前組件的實例上掛載__hooks
屬性。__hooks
爲一個對象,__hooks
對象中的_list
屬性使用數組的形式,保存了所有類型hooks(useState, useEffect…………)
的執行的結果,返回值等。因爲_list
屬性是使用數組的形式存儲狀態,所以每一個 hooks 的執行順序尤爲重要。
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
// 檢查組件,是否有__hooks屬性,如果沒有,主動掛載一個空的__hooks對象
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = {
_list: [], // _list中存儲了所有hooks的狀態
_pendingEffects: [], // _pendingEffects中存儲了useEffect的state
_pendingLayoutEffects: [], // _pendingLayoutEffects中存儲了useLayoutEffects的state
_handles: []
});
// 根據索引index。判斷__hooks._list數組中,是否有對應的狀態。
// 如果沒有,將主動添加一個空的狀態。
if (index >= hooks._list.length) {
hooks._list.push({});
}
// 返回__hooks._list數組中,索引對應的狀態
return hooks._list[index];
}
一些需要使用到的關鍵全局變量
在getHookState
中,我們使用了全局變量currentComponent
。變量currentComponent
指向的是當前的組件的實例。我們是如何拿到當前組件實例的引用的呢?結合 hooks 的源碼以及 preact 源碼後發現,當 preact 進行diff
時,會將當前組件的虛擬節點 VNode,傳遞給 options._render 函數,這樣我們就可以順利獲取當前組件的實例了。
// 當前hooks的執行順序指針
let currentIndex;
// 當前的組件的實例
let currentComponent;
let oldBeforeRender = options._render;
// vnode是
options._render = vnode => {
if (oldBeforeRender) oldBeforeRender(vnode);
// 當前組件的實例
currentComponent = vnode._component;
// 重置索引,每一個組件hooks state list從0開始累加
currentIndex = 0;
if (currentComponent.__hooks) {
currentComponent.__hooks._pendingEffects = handleEffects(
currentComponent.__hooks._pendingEffects
);
}
};
// 省略後的diff方法
function diff() {
let tmp, c;
// ...
// 在VNode上掛載當前組件的實例
newVNode._component = c = new Component(newProps, cctx);
// ...
// 將VNode傳遞給options._render函數, 這樣我們就可以拿到當前組件的實例
if ((tmp = options._render)) tmp(newVNode);
}
useState && useReducer
useState
useState
是基於useReducer
的封裝。詳情請看下面的useReducer
// useState接受一個初始值initialState,初始化state
function useState(initialState) {
return useReducer(invokeOrReturn, initialState);
}
invokeOrReturn
invokeOrReturn
是一個簡單的工具函數,這裏不作贅述。
function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
useReducer
useReducer
接受三個參數。reducer
負責處理dispatch
發起的action
,initialState
是state
狀態的初始值,init
是惰性化初始值的函數。useReducer
返回[state, dispatch]
格式的內容。
function useReducer(reducer, initialState, init) {
// currentIndex自增一,創建一個新的狀態,狀態會存儲在currentComponent.__hooks._list中
const hookState = getHookState(currentIndex++);
if (!hookState._component) {
// state存儲當前組件的引用
hookState._component = currentComponent;
hookState._value = [
// 如果沒有指定第三個參數`init, 返回initialState
// 如果指定了第三個參數,返回,經過惰性化初始值的函數處理的initialState
// `useState`是基於`useReducer`的封裝。
// 在`useState`中,hookState._value[0],默認直接返回initialState
!init ? invokeOrReturn(null, initialState) : init(initialState),
// hookState._value[1],接受一個`action`, { type: `xx` }
// 由於`useState`是基於`useReducer`的封裝,所以action參數也可能是一個新的state值,或者state的更新函數作爲參數
action => {
// 返回新的狀態值
const nextValue = reducer(hookState._value[0], action);
// 使用新的狀態值,更新狀態
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
// ⭐️調用組件的setState, 重新進行diff運算(在Preact中,diff的過程中會同步更新真實的dom節點)
hookState._component.setState({});
}
}
];
}
// 對於useReduer而言, 返回[state, dispath]
// 對於useState而言,返回[state, setState]
return hookState._value;
}
⭐️useEffect
useEffect
可以讓我們在函數組件中執行副作用操作。事件綁定,數據請求,動態修改 DOM。useEffect
將會在每一次 React 渲染之後執行。無論是初次掛載時,還是更新。useEffect 可以返回一個函數,當 react 進行清除時, 會執行這個返回的函數。每當執行本次的 effect 時,都會對上一個 effect 進行清除。組件卸載時也會執行進行清除。
function useEffect(callback, args) {
// currentIndex自增1,向currentComponent.__hooks._list中增加一個新的狀態
const state = getHookState(currentIndex++);
// argsChanged函數,會檢查useEffect的依賴是否發生了變化。
// 如果發生了變化,argsChanged返回true,會重新執行useEffect的callback。
// 如果沒有變化,argsChanged返回false,不執行callback
// 在第一次渲染中,state._args等於undefined的,argsChanged直接返回true
if (argsChanged(state._args, args)) {
state._value = callback;
// 在useEffect的state中保存上一次的依賴,下一次會使用它進行比較
state._args = args;
// 將useEffect的state存儲到__hooks._pendingEffects中
currentComponent.__hooks._pendingEffects.push(state);
// 把需要執行useEffect的callback的組件,添加到到afterPaintEffects數組中暫時保存起來
// 因爲我們需要等待渲染完成後,執行useEffect的callback
afterPaint(currentComponent);
}
}
argsChanged
argsChanged
是一個簡單的工具函數, 用來比較兩個數組之間的差異。如果數組中每一項相等返回 false,如果有一項不相等返回 true。主要用途是比較 useEffect,useMemo 等 hooks 的依賴。
function argsChanged(oldArgs, newArgs) {
return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}
afterPaint
afterPaint
函數,負責將需要執行useEffect
的callback的componennt,push到全局afterPaintEffects
數組中。
let afterPaintEffects = [];
let afterPaint = () => {};
if (typeof window !== "undefined") {
let prevRaf = options.requestAnimationFrame;
afterPaint = component => {
if (
// _afterPaintQueued屬性,確保了每一個component只能被push一次到afterPaintEffects中
(!component._afterPaintQueued &&
(component._afterPaintQueued = true) &&
// afterPaintEffects.push(component) === 1,確保了在清空前`safeRaf`只會被執行一次
// 將component添加到afterPaintEffects數組中
afterPaintEffects.push(component) === 1) ||
prevRaf !== options.requestAnimationFrame
) {
prevRaf = options.requestAnimationFrame;
// 執行safeRaf(flushAfterPaintEffects)
(options.requestAnimationFrame || safeRaf)(flushAfterPaintEffects);
}
};
}
safeRaf
safeRaf
會開啓一個requestAnimationFrame
,它會在diff(在Preact中的diff是同步的過程,相當於一個宏任務)完成後,調用flushAfterPaintEffects
,處理useEffect的callback。
const RAF_TIMEOUT = 100;
function safeRaf(callback) {
const done = () => {
clearTimeout(timeout);
cancelAnimationFrame(raf);
setTimeout(callback);
};
const timeout = setTimeout(done, RAF_TIMEOUT);
// diff過程是同步的,requestAnimationFrame將會在diff完成後(宏任務完成後)執行
const raf = requestAnimationFrame(done);
}
flushAfterPaintEffects
flushAfterPaintEffects
負責處理afterPaintEffects
數組中的所有組件
function flushAfterPaintEffects() {
// 循環處理afterPaintEffects數組中,所有待處理的component
afterPaintEffects.some(component => {
component._afterPaintQueued = false;
if (component._parentDom) {
// 使用handleEffects清空currentComponent.__hooks._pendingEffects中所有的useEffect的state
// handleEffects會進行清除effect和執行effect的邏輯
// handleEffects最後會返回一個空數組,重置component.__hooks._pendingEffects
component.__hooks._pendingEffects = handleEffects(
component.__hooks._pendingEffects
);
}
});
// 清空afterPaintEffects
afterPaintEffects = [];
}
handleEffects
清除和執行組件的useEffect
function handleEffects(effects) {
// 清除effect
effects.forEach(invokeCleanup);
// 執行所有的effect
effects.forEach(invokeEffect);
return [];
}
invokeCleanup
// 執行清除effect
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
invokeEffect
function invokeEffect(hook) {
const result = hook._value();
// 如果useEffect的callback的返回值是一個函數
// 函數會被記錄到useEffect的_cleanup屬性上
if (typeof result === "function") {
hook._cleanup = result;
}
}
useMemo && useCallback
useMemo
會返回一個memoized值。useCallback
會返回一個memoized回調函數。useMemo
會在依賴數組發生變化的時候,重新計算memoized值。useCallback
會在依賴數組發生變化的時候,返回一個新的函數。
useMemo
function useMemo(callback, args) {
// currentIndex自增1,向currentComponent.__hooks._list中增加一個新的狀態
const state = getHookState(currentIndex++);
// 判斷依賴數組是否發生變化
// 如果發生了變化,會重新執行callback,返回新的返回值
// 否則返回上一次的返回值
if (argsChanged(state._args, args)) {
state._args = args;
state._callback = callback;
// state._value記錄上一次的返回值(對於useCallback而言,記錄上一次的callback)
return state._value = callback();
}
// 返回callback的返回值
return state._value;
}
useCallback
useCallback
是基於useMemo
的封裝。只有當依賴數組產生變化時,useCallback
纔會返回一個新的函數,否則始終返回第一次的傳入callback。
function useCallback(callback, args) {
return useMemo(() => callback, args);
}
useRef
useRef
同樣是是基於useMemo
的封裝。但不同的是,依賴數組傳入的是一個空數組,這意味着,每一次useRef
都會重新計算。
function useRef(initialValue) {
return useMemo(() => ({ current: initialValue }), []);
}
useRef的應用
⭐️正是因爲useRef每一次都會重新計算,我們可以利用特性,避免閉包帶來的副作用
// 會打印出舊值
function Bar () {
const [ count, setCount ] = useState(0)
const showMessage = () => {
console.log(`count: ${count}`)
}
setTimeout(() => {
// 打印的出的依然是`0`, 形成了閉包
showMessage()
}, 2000)
setTimout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 1000)
return <div/>
}
// 利用useRef會打印出新值
function Bar () {
const count = useRef(0)
const showMessage = () => {
console.log(`count: ${count.current}`)
}
setTimeout(() => {
// 打印的出的是新值`1`,count.current拿到的是最新的值
showMessage()
}, 2000)
setTimout(() => {
count.current += 1
}, 1000)
return <div/>
}
useLayoutEffect
useEffec
t會在diff算法完成對dom渲染後執行。與useEffect
不同的是,useLayoutEffect
會在diff算法完成對dom更新之後,瀏覽器繪製之前的時刻執行。useLayoutEffect
是如何做到呢?和獲取當前組件的方法類似,preact會在diff算法最後返回dom前,插入了一個options.diffed
的鉤子。
function useLayoutEffect(callback, args) {
// currentIndex自增1,向currentComponent.__hooks._list中增加一個新的狀態
const state = getHookState(currentIndex++);
// 如果依賴數組,沒有變化跳過更新
// 如果依賴數組,參生變化執行callback
if (argsChanged(state._args, args)) {
state._value = callback;
// 記錄前一次的依賴數組
state._args = args;
currentComponent.__hooks._pendingLayoutEffects.push(state);
}
}
// options.diffed會在diff算法,完成對瀏覽器的重繪前更新
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
hooks._handles = bindHandles(hooks._handles);
// 執行組件的useLayoutEffects的callback
hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
}
};
// 省略後的diff方法
function diff() {
let tmp, c;
// ...
// ...
// 在瀏覽器繪製前,diff算法更新後,執行useLayoutEffect的callback
if (tmp = options.diffed) tmp(newVNode);
// 返回更新後的dom, 瀏覽器重繪
return newVNode._dom;
}
useImperativeHandle
useImperativeHandle
可以自定義向父組件暴露的實例值。useImperativeHandle
應當與forwardRef
一起使用。所以我們首先看一下preact中forwardRef
的具體實現。
forwardRef
forwardRef會創建一個React組件,組件接受ref屬性,但是會將ref轉發到組件的子節點上。我們ref訪問到子節點上的元素實例。
forwardRef的使用方式
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
))
const ref = React.createRef()
// 組件接受ref屬性,但是會將ref轉發到<button>上
<FancyButton ref={ref}>Click me!</FancyButton>
Preact中forwardRef的源碼
// fn爲渲染函數,接受(props, ref)作爲參數
function forwardRef(fn) {
function Forwarded(props) {
// props.ref是forwardRef創建的組件上的ref
let ref = props.ref;
delete props.ref;
// 調用渲染函數,渲染組件,並將ref轉發給渲染函數
return fn(props, ref);
}
Forwarded.prototype.isReactComponent = true;
Forwarded._forwarded = true;
Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')';
return Forwarded;
}
useImperativeHandle && bindHandles
function useImperativeHandle(ref, createHandle, args) {
// // currentIndex自增1,向currentComponent.__hooks._list中增加一個新的狀態
const state = getHookState(currentIndex++);
// 判斷依賴是否產生了變化
if (argsChanged(state._args, args)) {
// 在useEffect的state中保存上一次的依賴,下一次會使用它進行比較
state._args = args;
// 將useImperativeHandle的state添加到__hooks._handles數組中
// ref,是forwardRef轉發的ref
// createHandle的返回值,是useImperativeHandle向父組件暴露的自定義值
currentComponent.__hooks._handles.push({ ref, createHandle });
}
}
// options.diffed中調用bindHandles,對__hooks._handles處理
function bindHandles(handles) {
handles.some(handle => {
if (handle.ref) {
// 對forwardRef轉發的ref的current進行替換
// 替換的內容就是useImperativeHandle的第二個參數的返回值
handle.ref.current = handle.createHandle();
}
});
return [];
}
舉一個例子🌰
function Bar(props, ref) {
useImperativeHandle(ref, () => ({
hello: () => {
alert('Hello')
}
}));
return null
}
Bar = forwardRef(Bar)
function App() {
const ref = useRef('')
setTimeout(() => {
// useImperativeHandle會修改ref的current值
// current值是useImperativeHandle的第二個參數的返回值
// 所以我們可以調用useImperativeHandle暴露的hello方法
ref.current.hello()
}, 3000)
return <Bar ref={ref}/>
}