這是一篇譯文, 原文地址
做爲一種改變組件狀態、處理組件反作用的方式,Hooks這個概念最先由React提出,然後被推廣到其餘框架,諸如Vue、Svelte,甚至出現了原生JS庫。可是,要熟練使用Hooks須要對JS閉包有比較好的理解。javascript
在這篇文章中,咱們經過造一個迷你React Hooks輪子來說解閉包的應用。這麼作主要有2個目的:css
文章最後咱們會講解下自定義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個函數,state和setState。state返回內部變量_val的值,setState將該內部變量設置爲傳參的值。react
咱們這裏將state實現爲一個getter函數,這不是很理想,但沒有關係,咱們待會兒會改進他。重點是咱們經過保存對useState做用域的訪問,可以讀寫內部變量_val,這個引用就叫作閉包。在React和其餘的框架中,這看起來像state,其實,這就是state。git
若是你想更深刻的瞭解閉包,我推薦閱讀MDN,DailyJS。若是隻是爲了理解這篇文章,那理解上面這個例子就夠了。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了!
到目前爲止,咱們實現了第一個基礎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函數。
如今你能夠理解Hook規則的第一條:只在最頂層使用 Hook。經過currentHook變量咱們模擬了React對調用順序的依賴。你能夠通讀Hook規則詳細說明並回憶咱們的代碼實現,相信你會徹底明白的。
同時也請注意下第二條規則,只在 React 函數中調用 Hook,在咱們的迷你React雖然不是必要的,但遵照這條規則能夠確保組件的狀態邏輯在代碼中清晰可見(同時做爲一個好的反作用,遵照規則二能夠幫你更容易寫出遵照規則一的自定義Hook。由於這使你不容易在循環、條件語句中寫出像通常JS函數同樣命名的有狀態函數組件,遵照規則二幫助你遵照了規則一)。
咱們已經作了足夠多的練習了。如今你能夠試試一行代碼實現 useRef,或實現一個接受JSX並渲染到DOM上的render函數,或者其餘咱們的迷你React忽略的重要細節。但願經過本文你瞭解瞭如何在上下文中使用閉包,以及React Hooks的工做原理。