從實現到理解 React Hook 使用規則

寫在前面html

閱讀hook的官方文檔 能夠看到它的一些使用限制react

平時開發的時候,咱們會遵循這些使用規則,可是一般咱們不太理解其中的緣由,當咱們嘗試在循環或者條件語句中調用hook的時候, 瀏覽器就會拋出相似的錯誤。git

接下來 ,讓咱們嘗試手動實現如下useState和useEffect這兩個Hook。經過理解其設計原則來理解其使用規則.github

useState實現數組

參考以下計數器瀏覽器

setState須要返回一個[變量,函數]元組,而且在每次 調用函數時,自動調用render方法更新視圖bash

function App() {
  const [num, setNum] = useState < number > 0;

  return (
    <div>
      <div>num: {num}</div>
      <button onClick={() => setNum(num + 1)}>+ 1</button>
    </div>
  );
}

複製代碼
// Example 1 這麼作是有bug的!
function useState(initialValue) {
    var _val = initialValue
    function setState(newVal) {
      _val = newVal
      render()
    }
    return [_val, setState] // 直接對外暴露_val
  }
  var [foo, setFoo] = useState(0)
  console.log(foo) // logs 0 不須要進行函數調用
  setFoo(1) // 在useState做用域內給_val賦值
  console.log(foo) // logs 0 - 糟糕!!
複製代碼

這裏實現的setState每次更新state後,並不能拿到最新的state,由於組件內部的state值,已經state的值等於解構賦值時的初始值,後續沒有從新賦值給解構出來的變量,所以不會更新。閉包

爲了達到更新state的目的,而這個state又必須是一個變量而不是一個函數,咱們考慮把useState自己放在一個閉包中。函數

// Example 2
const MyReact = (function() {
    let _val // 將咱們的狀態保持在模塊做用域中
    return {
      render(Component) {
        const comp = Component()
        comp.render()
        return comp
      },
      useState(initialValue) {
        _val = _val || initialValue // 每次運行都從新賦值
        function setState(newVal) {
          _val = newVal
        }
        return [_val, setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      return {
          click : ()=>{b(a+1)},
          render:()=> {console.log(a)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
複製代碼

MyReact是容許用來渲染react組件的一個對象,當經過setState更新狀態後,每次渲染都會從新執行MyReact.useState ,以此拿到MyReact閉包內的最新數據。更新state到視圖oop

此時咱們的state只支持一個usestate 咱們經過將存放狀態的變量_val改形成數組和使用當前所在的usestate索引curr使其支持多個useState

// Example 3
const MyReact = (function() {
    let _val = [] // 將咱們的狀態保持在模塊做用域中
    let curr = 0
    return {
      render(Component) {
      // 每次更新視圖都須要重置curr
        curr = 0
        const comp = Component()
        comp.render()
        return comp
      },
      useState(initialValue) {
      // 注意這裏須要保存curr到本地變量currenCursor,不然當用戶setState時,curr已經不是咱們所須要的值
        const currenCursor = curr;
        _val[currenCursor] = _val[currenCursor] || initialValue; // 檢查是否渲染過
        function setState(newVal) {
          _val[currenCursor] = newVal
        }
        ++curr; 
        return [_val[currenCursor], setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      var [c,d] =  MyReact.useState(6)
      return {
          click : ()=>{b(a+1),d(c+1)},
          render:()=> {alert(a + '+' +c)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
複製代碼

至此 這個功能更像React中的useState Hook了

那麼如何解釋,不能再循環或者條件語句中使用useState呢

假設有以下代碼

let tag = true;

function App() {
  const [num, setNum] = useState < number > 0;

  // 只有初次渲染,才執行
  if (tag) {
    const [unusedNum] = useState < number > 1;
    tag = false;
  }

  const [num2, setNum2] = useState < number > 2;

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>加 1</button>
        <button onClick={() => setNum(num - 1)}>減 1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={() => setNum2(num2 * 2)}>擴大一倍</button>
        <button onClick={() => setNum2(num2 / 2)}>縮小一倍</button>
      </div>
    </div>
  );
}
複製代碼

第一次循環時 拿到的state&對應的cursor以下

state - setState中的cursor

num -0

unusedNum - 1

num2 - 2

第二次循環時得到的對應關係以下

state - setState中的cursor

num - 0

num2 - 1

此時的setNum2指向的對象不正確,天然update後返回的state也不正確。

實際上,react使用的是單向鏈表維護hook的前後順序和內容,但緣由是一致的,咱們能夠看看react實際源碼

ReactFiberHooks.js中定義了初始化時, firstWorkInProgressHook 和 workInProgressHook這兩個全局變量,觀察全部的hook實現

firstWorkInProgressHook指向第一個hook,workInProgressHook 指向當前正在處理的hook

每一個hook的結構能夠看開頭的Hook type定義

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
複製代碼

考慮下面的代碼

let condition = true;
const [state1,setState1] = useState(0);
if(condition){
    const [state2,setState2] = useState(1);
    condition = false;
}
const [state3,setState3] = useState(2);
複製代碼

初始化時 執行了如下流程

  • 初始時,組件還未渲染時,firstWorkInProgressHook = workInProgressHook = null;
  • firstWorkInProgressHook = workInProgressHook = Hook{state1}
  • firstWorkInProgressHook = workInProgressHook = Hook{state2}
  • firstWorkInProgressHook = workInProgressHook = Hook{state3}

能夠用下圖理解這個結構

memoizedState 存儲當前Hook的結果,next指向下一個hook,每一個hook對象中保存了當前hook的狀態 ,和指向下一個hook的屬性。

第二次渲染時,condition不符合,只剩下兩個hook 渲染調用update方法,update方法對每一個hook分別執行了updateWorkInProgressHook()

function updateWorkInProgressHook(): Hook {
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it. workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; nextCurrentHook = currentHook !== null ? currentHook.next : null; } else { // Clone from the current hook. invariant( nextCurrentHook !== null, 'Rendered more hooks than during the previous render.', ); currentHook = nextCurrentHook; const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list. workInProgressHook = firstWorkInProgressHook = newHook; } else { // Append to the end of the list. workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; } return workInProgressHook; } 複製代碼

currentHook 指向當前代碼中正在處理的hook,

workInProgressHook 指向以前維護的鏈表中對應的當前正在處理的hook

firstWorkInProgressHook 對應維護的鏈表結構對應的hook頭

更新時 第一個currentHook指向的next爲原先的第三個hook,而且賦值給鏈表結構中的第二個workInProgressHook對象,致使返回的state和setstate對象都是錯誤的!

useEffect實現

useEffect 稱爲組件的反作用,他相對於直接將邏輯寫在函數組件頂層的優勢是,能夠在依賴未更新的時候不重複執行。

在Example3 的基礎上加上useeffect的部分

_val 中 useEffect對應的內容爲傳入的依賴數組

每次執行時 對比以前的依賴和如今的值是否不一樣,而且更新依賴值,

若是更新 執行callback,而且更新依賴的最新值,不然什麼都不作

// Example 4
const MyReact = (function() {
    let _val = [] // 將咱們的狀態保持在模塊做用域中
    let curr = 0
    return {
      render(Component) {
        curr = 0
        const comp = Component()
        comp.render()
        return comp
      },
      // + effect部分
      useEffect(callback, depArray) {
        const currenCursor = curr;
        const hasNoDeps = !depArray
        const deps = _val[currenCursor] // type: array | undefined
        const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
        if (hasNoDeps || hasChangedDeps) {
          callback()
          _val[currenCursor] = depArray
        }
        curr++ // 本hook運行結束
      },
      // - effect部分
      useState(initialValue) {
        const currenCursor = curr;
        _val[currenCursor] = _val[currenCursor] || initialValue; // 檢查是否渲染過
        function setState(newVal) {
          _val[currenCursor] = newVal
        }
        ++curr; 
        return [_val[currenCursor], setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      MyReact.useEffect(() => {
        alert('effect:' +  a)
      }, [a])
      return {
          // 觸發effect
          click : ()=>{b(a+1)},
          // 不觸發effect
          noop: () => b(a),
          render:()=> {alert(a)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
  App.noop()
  App = MyReact.render(Counter)
複製代碼

以上的邏輯解釋了,爲何useeffect在不傳參數的時候,會在每次rerender時執行,而在傳[]時,只在初始化時執行一次。

總結

  • 本文使用數組 + cursor維護多個hooks的狀態,使用模塊閉包實現狀態值的更新。
  • 文章中使用的demo是簡化版的實現,而且在實際react中使用的是單向鏈表維護的hooks,demo爲最簡單實現,方便理解,useState 和 useEffect 特性洗
  • 歡迎批評 歡迎指正 🐶

參考文章

www.netlify.com/blog/2019/0…

github.com/SunShinewyf…

做者

做者主頁

相關文章
相關標籤/搜索