29行代碼深刻React Hooks原理

這是一篇譯文, 原文地址  

做爲一種改變組件狀態、處理組件反作用的方式,Hooks這個概念最先由React提出,然後被推廣到其餘框架,諸如Vue、Svelte,甚至出現了原生JS庫。可是,要熟練使用Hooks須要對JS閉包有比較好的理解。javascript

在這篇文章中,咱們經過造一個迷你React Hooks輪子來說解閉包的應用。這麼作主要有2個目的:css

  1. 展現閉包如何在Hooks內使用
  2. 展現一下如何用29行代碼實現一個迷你React Hooks

文章最後咱們會講解下自定義Hooks是如何回事。html

⚠️你不須要跟着敲一遍代碼就能理解Hooks,雖然這麼作能幫你鞏固下JS基礎。別擔憂,這一點都不難!

閉包是什麼?

Hooks的主要賣點之一是能夠避免複雜的Class組件和高階組件。可是有些人以爲使用Hooks有點像從一個坑裏進到另外一個坑裏。雖然不用擔憂Class組件的this指向的問題,可是又得擔憂閉包的引用,so sad~前端

儘管閉包是JS的基礎概念,但仍有不少前端萌新對其似懂非懂。Kyle Simpson在他的著做《你不知道的JS》中這麼定義閉包:vue

當代碼已經執行到一個函數詞法做用域以外,可是這個函數仍然能夠記住並訪問他的詞法做用域,那麼他就造成了閉包。

因此說閉包的概念和詞法做用域是緊密聯繫的,這是MDN對其的定義。咱們來看一個例子:java

// Demo 0
function useState(initialValue) {
  var _val = initialValue // _val是useState建立的本地變量
  function state() {
    // state是一個閉包
    return _val // state() 使用了由外層函數定義的_val
  }
  function setState(newVal) {
    // 一樣
    _val = newVal // 賦值_val
  }
  return [state, setState] // 將函數暴露給外部使用
}
var [foo, setFoo] = useState(0) // 數組解構
console.log(foo()) // 打印 0 - initialValue(初始值)
setFoo(1) // 在useState做用域內設置_val的值
console.log(foo()) // 打印 1 - 雖然調用同一個方法,但返回新的 initialValue
複製代碼

如今咱們有了第一版的React useState hook。在咱們的函數內部有2個函數,statesetStatestate返回內部變量_val的值,setState將該內部變量設置爲傳參的值。react

咱們這裏將state實現爲一個getter函數,這不是很理想,但沒有關係,咱們待會兒會改進他。重點是咱們經過保存對useState做用域的訪問,可以讀寫內部變量_val,這個引用就叫作閉包。在React和其餘的框架中,這看起來像state,其實,這就是stategit

若是你想更深刻的瞭解閉包,我推薦閱讀MDNDailyJS。若是隻是爲了理解這篇文章,那理解上面這個例子就夠了。github

在函數式組件中使用

讓咱們用咱們剛纔實現的useState來實現一個Counter組件。npm

// Demo 1
function Counter() {
  const [count, setCount] = useState(0) // 和上文實現的同樣
  return {
    click: () => setCount(count() + 1),
    render: () => console.log('render:', { count: count() })
  }
}
const C = Counter()
C.render() // render: { count: 0 }
C.click()
C.render() // render: { count: 1 }
複製代碼

咱們的render方法沒有渲染DOM,而是簡單的打印咱們的state。同時咱們暴露click方法來替代綁定點擊事件。經過以上方法咱們模擬了組件的渲染和點擊。

雖然代碼能夠運行,但把state設計爲一個getter並不符合React.useState的表現,咱們來改造他!

失效的閉包

若是要和實際的React API保持一致,咱們的state須要設計爲一個變量而不是一個函數。但若是隻是簡單的返回_val,咱們會遇到一個bug:

// Demo 0, 有bug的版本
function useState(initialValue) {
  var _val = initialValue
  // 去掉state函數
  function setState(newVal) {
    _val = newVal
  }
  return [_val, setState] // 直接返回 _val
}
var [foo, setFoo] = useState(0)
console.log(foo) // 打印 0
setFoo(1) // sets 將useState做用域內的_val賦值爲1
console.log(foo) // 打印 0 - 杯具!!
複製代碼

這是一種閉包失效的問題。當咱們解構了useState的返回值,他引用了useState調用時的函數內部的_val,這是個值引用,因此不會再改變了!這並非咱們想要的。總的來講,咱們但願組件state可以實時反應最新的state,同時又不能是一個函數調用!這兩個目標看起來是徹底相悖的。

模塊內部的閉包

要解決咱們面對的useState難題,咱們能夠把咱們的閉包...放到另外一個閉包裏嘛(認真臉)

// Demo 2
const MyReact = (function() {
  let _val // 在函數做用域內保存_val
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue // 每次執行都會賦值
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()
複製代碼

這裏咱們使用模塊模式構建咱們的新版本。他能夠像React同樣追蹤組件的狀態(在咱們的例子裏,他只會追蹤一個組件,內部在_val中保存state)。這樣的設計使得咱們的迷你React能夠「render」函數式組件,而且每次正確的爲內部變量_val賦值。

// 繼續 Demo 2
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count })
  }
}
let App
App = MyReact.render(Counter) // render: { count: 0 }
App.click()
App = MyReact.render(Counter) // render: { count: 1 }
複製代碼

如今看起來更像React Hooks了!

實現useEffect

到目前爲止,咱們實現了第一個基礎hook——useState。讓咱們來看看另外一個一樣很重要的Hook——useEffect。不一樣於useState,useEffect的執行是異步的,這意味着更有可能出現閉包的問題。

一塊兒來擴展咱們的React吧:

// Demo 3
const MyReact = (function() {
  let _val, _deps // 在做用域內部保存狀態和依賴
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        _deps = depArray
      }
    },
    useState(initialValue) {
      _val = _val || initialValue
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

// 使用
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  MyReact.useEffect(() => {
    console.log('effect', count)
  }, [count])
  return {
    click: () => setCount(count + 1),
    noop: () => setCount(count),
    render: () => console.log('render', { count })
  }
}
let App
App = MyReact.render(Counter)
// effect 0
// render {count: 0}
App.click()
App = MyReact.render(Counter)
// effect 1
// render {count: 1}
App.noop()
App = MyReact.render(Counter)
// // no effect run
// render {count: 1}
App.click()
App = MyReact.render(Counter)
// effect 2
// render {count: 2}
複製代碼

爲了追蹤依賴(當依賴變化時useEffect會從新執行),咱們引入了另外一個變量_deps

不是什麼魔法,僅僅是數組

咱們很好的實現了useState和useEffect,但他們的單例模式實現的不太好(只能存在一個不然就會出bug),爲了最終效果,咱們但願迷你React能夠接收任意數量的state和effect。幸運的是,React Hooks不是魔法,他僅僅是數組。因此,咱們將有一個hooks數組。同時,既然_val_deps是獨立的,咱們能夠把他們存儲在hooks數組裏。

// Demo 4
const MyReact = (function() {
  let hooks = [],
    currentHook = 0 // hooks數組 和 當前hook的索引
  return {
    render(Component) {
      const Comp = Component() // 執行 effects
      Comp.render()
      currentHook = 0 // 爲下一次render重置hook索引
      return Comp
    },
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const deps = hooks[currentHook] // type: array | undefined
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        hooks[currentHook] = depArray
      }
      currentHook++ // 當前hook處理完畢
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue // type: any
      const setStateHookIndex = currentHook // 爲了setState引用正確的閉包
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook++], setState]
    }
  }
})()
複製代碼

注意咱們定義的setStateHookIndex變量,雖然看起來沒有作任何事,但它能夠阻止setState引用到currentHook。若是不這麼作,setState會引用到錯誤的hook。(試試!)

// 繼續 Demo 4 - 使用
function Counter() {
  const [count, setCount] = MyReact.useState(0)
  const [text, setText] = MyReact.useState('foo') // 第二個 state hook!
  MyReact.useEffect(() => {
    console.log('effect', count, text)
  }, [count, text])
  return {
    click: () => setCount(count + 1),
    type: txt => setText(txt),
    noop: () => setCount(count),
    render: () => console.log('render', { count, text })
  }
}
let App
App = MyReact.render(Counter)
// effect 0 foo
// render {count: 0, text: 'foo'}
App.click()
App = MyReact.render(Counter)
// effect 1 foo
// render {count: 1, text: 'foo'}
App.type('bar')
App = MyReact.render(Counter)
// effect 1 bar
// render {count: 1, text: 'bar'}
App.noop()
App = MyReact.render(Counter)
// // 沒有effect執行
// render {count: 1, text: 'bar'}
App.click()
App = MyReact.render(Counter)
// effect 2 bar
// render {count: 2, text: 'bar'}
複製代碼

簡單來講,整個邏輯是經過一個hooks數組和一個每次調用hook都會遞增的索引,並在每次組件render後都重置索引。

你也能夠構造自定義hooks

// 再提一次 Demo 4
function Component() {
  const [text, setText] = useSplitURL('www.netlify.com')
  return {
    type: txt => setText(txt),
    render: () => console.log({ text })
  }
}
function useSplitURL(str) {
  const [text, setText] = MyReact.useState(str)
  const masked = text.split('.')
  return [masked, setText]
}
let App
App = MyReact.render(Component)
// { text: [ 'www', 'netlify', 'com' ] }
App.type('www.reactjs.org')
App = MyReact.render(Component)
// { text: [ 'www', 'reactjs', 'org' ] }}
複製代碼

這就是hooks「不是魔法」的緣由——不論是在React中仍是咱們的迷你React,自定義hook只是脫離框架的原生JS函數。

推導Hooks的規則

如今你能夠理解Hook規則的第一條:只在最頂層使用 Hook。經過currentHook變量咱們模擬了React對調用順序的依賴。你能夠通讀Hook規則詳細說明並回憶咱們的代碼實現,相信你會徹底明白的。

同時也請注意下第二條規則,只在 React 函數中調用 Hook,在咱們的迷你React雖然不是必要的,但遵照這條規則能夠確保組件的狀態邏輯在代碼中清晰可見(同時做爲一個好的反作用,遵照規則二能夠幫你更容易寫出遵照規則一的自定義Hook。由於這使你不容易在循環、條件語句中寫出像通常JS函數同樣命名的有狀態函數組件,遵照規則二幫助你遵照了規則一)。

結論

咱們已經作了足夠多的練習了。如今你能夠試試一行代碼實現 useRef,或實現一個接受JSX並渲染到DOM上的render函數,或者其餘咱們的迷你React忽略的重要細節。但願經過本文你瞭解瞭如何在上下文中使用閉包,以及React Hooks的工做原理。

相關文章
相關標籤/搜索