實現 React Hooks

liuyongjia發表於2020-07-06

實現 React Hooks

UI 開發有兩個問題:

  1. 展示覆用
  2. 邏輯複用

展示覆用目前基本使用元件化來解決,邏輯複用一直以來都沒有特別好的解決方案。React 從一開始的 mixin ,到 高階元件 以及 Render Props ,都是在試圖解決這個問題,但是都引入了一些別的問題。

Mixins

  1. 名稱空間衝突
  2. 資料來源不清晰

Higher-order Components

  1. props 屬性來源不清晰
  2. props 上命名衝突
  3. 額外的元件渲染帶來效能問題

Render Props(Vue 中的 Renderless Components)

  1. 解決了 名稱空間衝突、資料來源不清晰的問題,仍然會帶來額外元件例項的效能消耗

Hooks

在前段時間 Hooks 釋出後,我認為 React 找到了【有狀態】元件【函式式】【複用邏輯】的解決方案。
先說有狀態:一般來說,無狀態元件直接使用函式元件就行,省去了例項化的樣板程式碼和效能消耗。不涉及到 state 的存取,可以直接寫個 helper 函式處理一下,方便又快捷。
再說函式式:class 元件是物件導向的,每一次宣告、宣告週期都逃不開 this,而 hooks 更加函式式,呼叫一個函式,傳入的是初始值,返回修改值,沒有副作用。
最後說複用邏輯:DRY,一般來說,相同的程式碼不寫第二次,在 class 元件中,通過生命週期方法對 state 修改,然後 rerender,在使用了 hooks 以後,我們可以通過 hooks 觸發 render,render 呼叫 hooks 時,根據傳入值的比較,來決定是否觸發 render,然後把 hooks 返回的值填充到頁面上。

實現

hooks 給人最直接的印象就是可以在 function 元件中使用 state 了。但它也有著不同於 class 元件的心智模型,從生命週期的思維跳到 update circle,剛開始 useEffect 總會寫一些無限迴圈。接下來我們先來寫一個簡易版的 useState,來模仿 React。

let state;
function useState(init) {
  function setState(newState) {
    if (typeof newState === "function") {
      // 支援舊值傳入更新
      state = newState(state);
    } else {
      state = newState;
    }
    // setState 後呼叫元件 render
    // render();
  }
  if (!state) {
    state = init;
  }
  return [state, setState];
}

上面就是一個 useState 了,接下來繼續寫 useEffect.

let _deps;
function useEffect(callback, deps) {
  if (
    deps === undefined || // 就是不傳 deps,就是每次都執行副作用
    (deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
    !deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
  ) {
    callback();
  }
}

這就是 useEffect 基本實現了,當然還有一個問題,callback 的 return 函式問題。

let _deps;
function useEffect(callback, deps) {
  if (
    deps === undefined || // 就是不傳 deps,就是每次都執行副作用
    (deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
    !deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
  ) {
    // 如果有 cleanUp 就執行清理
    if (typeof _deps._cleanUp === "function") {
      _deps._cleanUp();
    }
    _deps._cleanUp = callback();
  }
}

接下來,還有個重要問題,hooks 的順序問題,其實就是把 state 和 deps 存到陣列裡。

let _arr = [];
let cursor = 0;
function useState(init) {
  const current = cursor;
  function setState(newState) {
    if (typeof newState === "function") {
      // 支援舊值傳入更新
      _arr[current] = newState(_arr[current]);
    } else {
      _arr[current] = newState;
    }
    // setState 後呼叫元件 render
    render();
  }
  if (!_arr[current]) {
    _arr[current] = init;
  }
  cursor++;
  return [_arr[current], setState];
}
function useEffect(callback, deps) {
  const effect = _arr[cursor] || {};
  const { _deps, _cleanUp } = effect;
  if (
    deps === undefined || // 就是不傳 deps,就是每次都執行副作用
    (deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
    !deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
  ) {
    // 如果有 cleanUp 就執行清理
    if (typeof _cleanUp === "function") {
      _cleanUp();
    }
    effect._cleanUp = callback();
    effect._deps = deps;
    _arr[cursor] = effect;
  }
  cursor++;
}

全部程式碼在 CodeSandBox
完。

相關文章