「Preact」逐行解析hooks源碼

前言

Preact 是什麼?React 的 3kb 輕量化方案,擁有一樣的 ES6APInode

雖然 Preact 和 React 有着相同的 API, 可是其內部實現機制的差別依然是巨大。可是這並不妨礙咱們閱讀以及學習 Preact 的源碼。說一句題外話,今年年初的時候,個人一位哥們@小寒,在北京某家公司面試時遇到了來自 Facebook 的大牛,這位 Facebook 的大牛也曾推薦過他,閱讀學習 Preact 的源碼。react

hooks 不是什麼魔法,hooks 的設計也與 React 無關(Dan Abramov)。在 Preact 中也是如此,因此即便你沒有閱讀過 Preact 或者 React 源碼,也不妨礙理解 hooks 的實現。面試

但願下面的分享,對你們理解 hooks 背後的實現能有所啓示。算法

關於 hooks 的規則

React 中 hooks 的使用規則以下。咱們能夠看出 hooks 的使用,高度的依賴執行順序。在閱讀完源碼後,咱們就會知道,爲何 hooks 的使用會有這兩條規則。數組

  1. ✅ 只在最頂層使用 hook。不要在循環,條件或嵌套函數中調用 hook。
  2. ✅ 不要在普通的 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的封裝。詳情請看下面的useReducerdom

// 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發起的actioninitialStatestate狀態的初始值,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

useEffect會在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}/> } 複製代碼

推薦閱讀

相關文章
相關標籤/搜索