從根本上說,Hooks是一種更簡單的方式,用于封裝用戶界面中的有狀態行為和副作用。React最先引入了Hooks,現在其他框架如Vue,Svelte都廣泛實現了該功能,TNG-Hooks甚至可以為常規的JS函數提供Hooks。然而它們的函數式設計需要對Javascript里的[閉包](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651228474&idx=1&sn=031ea46ca182f2dacf8f65cc30c6566b&chksm=bd4950be8a3ed9a87e24c664dec77bd63bb69e735887ea33dc574358070affb0fbf6eafd9f0b&scene=21#wechat_redirect)有很好的理解。
在本文,我們將使用閉包實現一個React Hooks的微型版本。這么做有兩個目的,一是演示閉包的效用,二是如何使用29行易讀的JS代碼實現Hooks。最后我們可以很自然的得到自定義Hooks。
## 閉包是什么?
Hooks的許多賣點之一是避免了類和高階組件的復雜性。然而有些人覺得Hooks可能會導致另外的問題。雖然不需要再擔心綁定上下文,現在我們需要擔心閉包。正如Mark Dalgleish令人難忘的總結:

閉包是JS里的基本概念。眾所周知對許多初學的開發者來說它們令人費解。Kyle Simpson在《你不知道的JS》中對閉包的著名定義如下:
閉包是:當一個函數在它的詞法作用域之外執行的時候,仍然可以記得它的詞法作用域且可以訪問該作用域。
閉包很顯然和詞法作用域的概念緊密相關,在MDN是這么描述詞法作用域的:“當函數被嵌套時,解析器解析函數的變量名的方式”。讓我們來看一個實際的例子,可以更好的說明這一點:
```
// Example 0
function useState(initialValue) {
var _val = initialValue //_val是useState創建的局部變量`
function state() {
// state 是一個內部函數, 也是一個閉包
return _val // state() 使用了_val, 該變量由父函數聲明
}
function setState(newVal) {
// 同樣是內部函數
9. `_val = newVal // 給_val賦值,而不用暴露_val`
10. `}`
11. `return [state, setState] //將這兩個函數暴露到外部`
12. `}`
13. `var [foo, setFoo] = useState(0) // 使用了數組解構方法`
14. `console.log(foo()) // logs 0 - 我們給的初始值`
15. `setFoo(1) // 在useState的作用域內給_val賦值`
16. `console.log(foo()) // logs 1 - 盡管使用了相同的函數調用,得到的是新的初始值`
```
這里,我們建立了React的 useState hook的原始版本。這里有2個內部函數,state和setState。state返回了上面定義的局部變量\_val,setState將傳給它的參數(即newVal)賦給該局部變量。
我們的state用getter函數實現,這并不完美,但我們將對此進行改進。這里重要的是,使用foo和setFoo,我們可以訪問和操作(即所謂的“封閉”)內部變量\_val。這兩個函數保留了useState作用域的訪問權,而這樣的引用被稱為閉包。放在在React和其他框架的上下文中,這看起來好像狀態,實際上正是如此。
#### 在函數式組件中的使用
讓我們來把剛做出的useState功能應用到常見的程序中。下面來做一個計數器組件!
```
1. `// Example 1`
2. `function Counter() {`
3. `const [count, setCount] = useState(0) // 和上面定義的 useState 相同`
4. `return {`
5. `click: () => setCount(count() + 1),`
6. `render: () => console.log('render:', { count: count() })`
7. `}`
8. `}`
9. `const C = Counter()`
10. `C.render() // render: { count: 0 }`
11. `C.click()`
12. `C.render() // render: { count: 1 }`
```
這里我們沒有把數據渲染到DOM上,而是僅在控制臺輸出這些狀態。我們讓計數器提供一個外部API,這樣我們可以直接運行腳本,而不必給它設置click事件處理函數。
雖然這種做法也可以工作起來,調用getter函數來訪問狀態并不是React.useState hook的實際做法。我們來改進它。
#### 不能更新狀態的閉包實現
如果我們想要做得和實際的React hook一樣,狀態就應該是一個變量,而不是函數。如果我們簡單的將\_val暴露出去,而不是將它包裹在函數里面,就會出現bug:
```
1. `// Example 0, 再來看第一個例子 - 這么做是有bug的!`
2. `function useState(initialValue) {`
3. `var _val = initialValue`
4. `// 不使用state()函數`
5. `function setState(newVal) {`
6. `_val = newVal`
7. `}`
8. `return [_val, setState] // 直接對外暴露_val`
9. `}`
10. `var [foo, setFoo] = useState(0)`
11. `console.log(foo) // logs 0 不需要進行函數調用`
12. `setFoo(1) // 在useState作用域內給_val賦值`
13. `console.log(foo) // logs 0 - 糟糕!!`
```
這是種閉包不能更新的問題。當我們從useState的輸出中解構出foo變量時,foo的值等于對useState初始調用時的\_val值,之后就不會再變了!這不是我們想要的結果,通常我們需要讓組件的狀態能反映出當前的狀態,而且狀態應該是一個變量而不是一個函數!這兩個目標看起來不可兼得。
#### 模塊模式的閉包實現
我們可以解決這一useState難題……通過將閉包放進另一個閉包中!(我的天!聽說你喜歡閉包……)
```
1. `// Example 2`
2. `const MyReact = (function() {`
3. `let _val // 將我們的狀態保持在模塊作用域中`
4. `return {`
5. `render(Component) {`
6. `const Comp = Component()`
7. `Comp.render()`
8. `return Comp`
9. `},`
10. `useState(initialValue) {`
11. `_val = _val || initialValue // 每次運行都重新賦值`
12. `function setState(newVal) {`
13. `_val = newVal`
14. `}`
15. `return [_val, setState]`
16. `}`
17. `}`
18. `})()`
```
這里我們選擇使用模塊模式來制作我們的微型React hook。像React一樣,它可以記錄組件的狀態(在這個例子中,它只能給每個組件記錄一個狀態,將狀態記錄在val中)。該設計允許MyReact渲染你的函數式組件,它可以在每次組件更新時使用和它相應的閉包,對內部的val賦值。
```
1. `// 續Example 2`
2. `function Counter() {`
3. `const [count, setCount] = MyReact.useState(0)`
4. `return {`
5. `click: () => setCount(count + 1),`
6. `render: () => console.log('render:', { count })`
7. `}`
8. `}`
9. `let App`
10. `App = MyReact.render(Counter) // render: { count: 0 }`
11. `App.click()`
12. `App = MyReact.render(Counter) // render: { count: 1 }`
```
現在看起來更像React里的Hooks了。
#### 復制useEffect功能
目前為止,我們已經實現了useState,這是最基本的React Hook。下一個重要的Hook是useEffect。不像setState,useEffect是異步執行的,這意味著更容易遇到閉包問題。
我們可以擴展這個微型React模型,加入下面代碼:
```
1. `// Example 3`
2. `const MyReact = (function() {`
3. `let _val, _deps // 在作用域內保持狀態和依賴`
4. `return {`
5. `render(Component) {`
6. `const Comp = Component()`
7. `Comp.render()`
8. `return Comp`
9. `},`
10. `useEffect(callback, depArray) {`
11. `const hasNoDeps = !depArray`
12. `const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true`
13. `if (hasNoDeps || hasChangedDeps) {`
14. `callback()`
15. `_deps = depArray`
16. `}`
17. `},`
18. `useState(initialValue) {`
19. `_val = _val || initialValue`
20. `function setState(newVal) {`
21. `_val = newVal`
22. `}`
23. `return [_val, setState]`
24. `}`
25. `}`
26. `})()`
27.
28. `// 使用方法`
29. `function Counter() {`
30. `const [count, setCount] = MyReact.useState(0)`
31. `MyReact.useEffect(() => {`
32. `console.log('effect', count)`
33. `}, [count])`
34. `return {`
35. `click: () => setCount(count + 1),`
36. `noop: () => setCount(count),`
37. `render: () => console.log('render', { count })`
38. `}`
39. `}`
40. `let App`
41. `App = MyReact.render(Counter)`
42. `// effect 0`
43. `// render {count: 0}`
44. `App.click()`
45. `App = MyReact.render(Counter)`
46. `// effect 1`
47. `// render {count: 1}`
48. `App.noop()`
49. `App = MyReact.render(Counter)`
50. `// // no effect run`
51. `// render {count: 1}`
52. `App.click()`
53. `App = MyReact.render(Counter)`
54. `// effect 2`
55. `// render {count: 2}`
```
為了追蹤依賴項(因為useEffect只有在依賴項發生變化才會重新運行callback),我們引入了另一個變量\_deps。
#### 沒有魔法,只是數組而已
我們已經很好的復制了useState和useEffect的功能,但是它們是實現得很差的單態(只允許一個狀態,一個副作用,多了就會有bug)。為了讓事情變得更有意思,我們需要擴展它使之可以接受任意數量的狀態和副作用。幸運的是,正如Rudi Yardley所寫的,React Hooks不是什么魔法,僅僅是數組而已。因此我們會使用到一個hooks數組。我們把val和deps全都放在同一個數組中,因為它們是互不干擾的。
```
1. `// Example 4`
2. `const MyReact = (function() {`
3. `let hooks = [],`
4. `currentHook = 0 // hooks數組, 和一個iterator!`
5. `return {`
6. `render(Component) {`
7. `const Comp = Component() // 運行 effects`
8. `Comp.render()`
9. `currentHook = 0 // 復位,為下一次render做準備`
10. `return Comp`
11. `},`
12. `useEffect(callback, depArray) {`
13. `const hasNoDeps = !depArray`
14. `const deps = hooks[currentHook] // type: array | undefined`
15. `const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true`
16. `if (hasNoDeps || hasChangedDeps) {`
17. `callback()`
18. `hooks[currentHook] = depArray`
19. `}`
20. `currentHook++ // 本hook運行結束`
21. `},`
22. `useState(initialValue) {`
23. `hooks[currentHook] = hooks[currentHook] || initialValue // type: any`
24. `const setStateHookIndex = currentHook // 給setState的閉包準備的變量!`
25. `const setState = newState => (hooks[setStateHookIndex] = newState)`
26. `return [hooks[currentHook++], setState]`
27. `}`
28. `}`
29. `})()`
```
注意我們這里使用的setStateHookIndex變量,看起來好像沒有什么用,但它是用來避免setState將currentHook直接封閉進去!如果你直接使用currentHook,setState功能不會正常工作,因為currentHook在每次[render](http://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651234633&idx=2&sn=b591767f7d9000b4d2e5eb72c3b4ce64&chksm=bd4978cd8a3ef1db155a41f0a2a3d45ce910a946587553d45ef7f1ddd3f3f8c57a72b967ed2d&scene=21#wechat_redirect)后都被復位為0,之后再調用setState則每次都將修改hook數組的第一項。
```
1. `// Example 4 續 - 使用hook`
2. `function Counter() {`
3. `const [count, setCount] = MyReact.useState(0)`
4. `const [text, setText] = MyReact.useState('foo') // 第二個 state hook!`
5. `MyReact.useEffect(() => {`
6. `console.log('effect', count, text)`
7. `}, [count, text])`
8. `return {`
9. `click: () => setCount(count + 1),`
10. `type: txt => setText(txt),`
11. `noop: () => setCount(count),`
12. `render: () => console.log('render', { count, text })`
13. `}`
14. `}`
15. `let App`
16. `App = MyReact.render(Counter)`
17. `// effect 0 foo`
18. `// render {count: 0, text: 'foo'}`
19. `App.click()`
20. `App = MyReact.render(Counter)`
21. `// effect 1 foo`
22. `// render {count: 1, text: 'foo'}`
23. `App.type('bar')`
24. `App = MyReact.render(Counter)`
25. `// effect 1 bar`
26. `// render {count: 1, text: 'bar'}`
27. `App.noop()`
28. `App = MyReact.render(Counter)`
29. `// // no effect run`
30. `// render {count: 1, text: 'bar'}`
31. `App.click()`
32. `App = MyReact.render(Counter)`
33. `// effect 2 bar`
34. `// render {count: 2, text: 'bar'}`
```
所以基本的思路是使用數組存放hook的狀態和依賴,調用每個hook只需增加索引號操作相應的數組項,當組件render完畢后復位索引。
還可以很容易的實現自定義hooks:
```
1. `// Example 4, revisited`
2. `function Component() {`
3. `const [text, setText] = useSplitURL('www.netlify.com')`
4. `return {`
5. `type: txt => setText(txt),`
6. `render: () => console.log({ text })`
7. `}`
8. `}`
9. `function useSplitURL(str) {`
10. `const [text, setText] = MyReact.useState(str)`
11. `const masked = text.split('.')`
12. `return [masked, setText]`
}
14. `let App`
15. `App = MyReact.render(Component)`
16. `// { text: [ 'www', 'netlify', 'com' ] }`
17. `App.type('www.reactjs.org')`
18. `App = MyReact.render(Component)`
19. `// { text: [ 'www', 'reactjs', 'org' ] }}`
```
這就是hooks的實際原理,自定義hooks只需簡單的利用框架提供的原語,不管是React里的還是我們這里制作的微型hook版本都是如此。
#### 使用hook的法則
現在你可以很容易理解使用Hooks的第一個法則:只在最頂層調用Hooks。因為我們使用currentHook變量,需要根據調用次序對React的依賴建模。你可以對照著我們的代碼實現,去閱讀Hooks法則的解釋,就可以完全理解所有內容。
第二條法則,“僅在React函數中調用Hooks”。使用我們這一實現方法,這條法則不是必須遵守的,但是明確的界定代碼的哪一部分依賴于有狀態的邏輯是相當好的實踐方式。(這也可以讓我們更容易編寫工具來確保遵守第一個法則。你不會在無意中包裹有狀態的函數,在循環和條件語句中當成一般的函數包裹它們。遵守第二條法則有助于遵守第一條法則)
#### 結論
到這里,我們已經把最初的例子擴展的很遠了。你可以嘗試使用一行代碼實現useRef,或者讓render函數接受JSX并掛載到DOM上,或者實現無數種其他重要的細節,在這28行的hook版本里我們忽略掉了。希望現在你已經收獲一些在上下文中使用閉包的經驗,并且在頭腦中有一個有用的模型,可以解釋React Hooks是如何工作的。