2019-11-15:
學習內容:
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。
Hook 是一些可以讓你在函數組件里“鈎入” React state 及生命周期等特性的函數。Hook 不能在 class 組件中使用 —— 這使得你不使用 class 也能使用 React。
一、簡介:
請記住 Hook 是:
- 完全可選的。 你無需重寫任何已有代碼就可以在一些組件中嘗試 Hook。但是如果你不想,你不必現在就去學習或使用 Hook。
- 100% 向后兼容的。 Hook 不包含任何破壞性改動。
- 現在可用。 Hook 已發布於 v16.8.0。
沒有計划從 React 中移除 class。
Hook 不會影響你對 React 概念的理解。 恰恰相反,Hook 為已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。
動機:
i、組件之間復用狀態邏輯很難:
React 沒有提供將可復用性行為“附加”到組件的途徑(例如,把組件連接到 store)。React 需要為共享狀態邏輯提供更好的原生途徑。
你可以使用 Hook 從組件中提取狀態邏輯,使得這些邏輯可以單獨測試並復用。Hook 使你在無需修改組件結構的情況下復用狀態邏輯。 這使得在組件間或社區內共享 Hook 變得更便捷。
ii、復雜組件變得難以理解:
組件起初很簡單,但是逐漸會被狀態邏輯和副作用充斥。每個生命周期常常包含一些不相關的邏輯。
相互關聯且需要對照修改的代碼被進行了拆分,而完全不相關的代碼卻在同一個方法中組合在一起。如此很容易產生 bug,並且導致邏輯不一致。
在多數情況下,不可能將組件拆分為更小的粒度,因為狀態邏輯無處不在。這也給測試帶來了一定挑戰。同時,這也是很多人將 React 與狀態管理庫結合使用的原因之一。但是,這往往會引入了很多抽象概念,需要你在不同的文件之間來回切換,使得復用變得更加困難。
為了解決這個問題,Hook 將組件中相互關聯的部分拆分成更小的函數(比如設置訂閱或請求數據),而並非強制按照生命周期划分。你還可以使用 reducer 來管理組件的內部狀態,使其更加可預測。
iii、class 是一個障礙:
理解 JavaScript 中 this 的工作方式,這與其他語言存在巨大差異。還不能忘記綁定事件處理器。
class 不能很好的壓縮,並且會使熱重載出現不穩定的情況。
為了解決這些問題,Hook 使你在非 class 的情況下可以使用更多的 React 特性。 從概念上講,React 組件一直更像是函數。而 Hook 則擁抱了函數,同時也沒有犧牲 React 的精神原則。Hook 提供了問題的解決方案,無需學習復雜的函數式或響應式編程技術。
二、使用 State Hook:
(1)state Hook: { useState }
例子:計數器,記錄按鍵次數

在這里,useState 就是一個 Hook 。通過在函數組件里調用它來給組件添加一些內部 state。React 會在重復渲染時保留這個 state。useState 會返回一對值:當前狀態和一個讓你更新它的函數,你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class 組件的 this.setState,但是它不會把新的 state 和舊的 state 進行合並。
useState 唯一的參數就是初始 state。在上面的例子中,我們的計數器是從零開始的,所以初始 state 就是 0。值得注意的是,不同於 this.state,這里的 state 不一定要是一個對象 —— 如果你有需要,它也可以是。這個初始 state 參數只有在第一次渲染時會被用到。
我們聲明了一個叫 count 的 state 變量,然后把它設為 0。React 會在重復渲染時記住它當前的值,並且提供最新的值給我們的函數。我們可以通過調用 setCount 來更新當前的 count。
(2)等價的class 示例:

state 初始值為 { count: 0 } ,當用戶點擊按鈕后,我們通過調用 this.setState() 來增加 state.count。
對比class和hook兩個例子:
i、在 class 中,我們通過在構造函數中設置 this.state 為 { count: 0 } 來初始化 count state 為 0;
ii、在函數組件中,我們沒有 this,所以我們不能分配或讀取 this.state。我們直接在組件中調用 useState Hook。
(const [count, setCount] = useState(0);)方括號是數組解構。
🌟(3)關於useState使用:
i、調用 useState 方法的時候做了什么? -- 它定義一個 “state 變量”。我們的變量叫 count, 但是我們可以叫他任何名字,比如 banana。這是一種在函數調用時保存變量的方式 —— useState 是一種新方法,它與 class 里面的 this.state 提供的功能完全相同。一般來說,在函數退出后變量就就會”消失”,而 state 中的變量會被 React 保留。
ii、useState 需要哪些參數? useState() 方法里面唯一的參數就是初始 state。不同於 class 的是,我們可以按照需要使用數字或字符串對其進行賦值,而不一定是對象。在示例中,只需使用數字來記錄用戶點擊次數,所以我們傳了 0 作為變量的初始 state。(如果我們想要在 state 中存儲兩個不同的變量,只需調用 useState() 兩次即可。)
iii、useState 方法的返回值是什么? 返回值為:當前 state 以及更新 state 的函數。這就是我們寫 const [count, setCount] = useState() 的原因。這與 class 里面 this.state.count 和 this.setState 類似,唯一區別就是你需要成對的獲取它們。
(4)為什么叫useState:
“Create” 可能不是很准確,因為 state 只在組件首次渲染的時候被創建。在下一次重新渲染時,useState 返回給我們當前的 state。否則它就不是 “state”了!這也是 Hook 的名字總是以 use 開頭的一個原因。
🌟(5)如何讀取state?
i、class中: { this.state.count }
ii、hook中: { count }
🌟(6)更新 state?
i、class中,用 this.setState( { count : this.state.count + 1 } )
ii、hook中,已經有setCount 和count 變量: setCount( count + 1 )
(7)為什么react知道這個hook對應哪個組件?
React 保持對當先渲染中的組件的追蹤。
當你用 useState() 調用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),然后把指針移動到下一個。這就是多個 useState() 調用會得到各自獨立的本地 state 的原因。
(8)用函數組件和hook 取代clss:

函數組件,曾經稱為“無狀態組件”,但現在為他們引入使用React state(hook造成的)。
三、使用 Effect Hook:(兩種常見副作用操作:需要清除的和不需要清除的)
(1)Effect Hook: { useEffect }
可能已經在 React 組件中執行過數據獲取、訂閱或者手動修改過 DOM。我們統一把這些操作稱為“副作用”,或者簡稱為“作用”。
useEffect 就是一個 Effect Hook,給函數組件增加了操作副作用的能力。它跟 class 組件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不過被合並成了一個 API。
舉例1: 上面例子添加功能--更新DOM后設置一個頁面標題:(運行“副作用”)

當你調用 useEffect 時,就是在告訴 React 在完成對 DOM 的更改后運行你的“副作用”函數。由於副作用函數是在組件內聲明的,所以它們可以訪問到組件的 props 和 state。默認情況下,React 會在每次渲染后調用副作用函數 —— 包括第一次渲染的時候。
(2)不需清除的effect:
在 React 更新 DOM 之后運行一些額外的代碼。比如發送網絡請求,手動變更 DOM,記錄日志,這些都是常見的無需清除的操作。因為我們在執行完這些操作之后,就可以忽略他們了。
class中的副作用實現:

render 函數是不應該有任何副作用的。我們基本上都希望在 React 更新 DOM 之后才執行我們的操作。這就是為什么在 React class 中,我們把副作用操作放到 componentDidMount 和 componentDidUpdate 函數中。
對比hook和class 在副作用中的區別:
i、useEffect 做了什么?-- 通過使用這個 Hook,你可以告訴 React 組件需要在渲染后執行某些操作。React 會保存你傳遞的函數(我們將它稱之為 “effect”),並且在執行 DOM 更新之后調用它。在這個 effect 中,我們設置了 document 的 title 屬性,不過我們也可以執行數據獲取或調用其他命令式的 API。
ii、為什么在組件內部調用 useEffect?-- 將 useEffect 放在組件內部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經保存在函數作用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供了解決方案的情況下,還引入特定的 React API。
iii、useEffect 會在每次渲染后都執行嗎?-- 是的,默認情況下,它在第一次渲染之后和每次更新之后都會執行。你可能會更容易接受 effect 發生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運行 effect 的同時,DOM 都已經更新完畢。
更快:
與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數情況下,effect 不需要同步地執行。
(3)需要清楚的 effect:
例如訂閱外部數據源。這種情況下,清除工作是非常重要的,可以防止引起內存泄露!
舉例2: (清除副作用)訂閱好友的在線狀態,並通過取消訂閱來清除操作:

React 會在組件銷毀時取消對 ChatAPI 的訂閱,然后在后續渲染時重新執行副作用函數。
通過使用 Hook,你可以把組件內相關的副作用組織在一起(例如創建訂閱及取消訂閱),而不要把它們拆分到不同的生命周期函數里。
在class中會這樣做:

componentDidMount 和 componentWillUnmount 之間相互對應。使用生命周期函數迫使我們拆分這些邏輯代碼,即使這兩部分代碼都作用於相同的副作用。
對比hook和class 的副作用寫法:
為什么要在 effect 中返回一個函數?-- 這是 effect 可選的清除機制。每個 effect 都可以返回一個清除函數。如此可以將添加和移除訂閱的邏輯放在一起。它們都屬於 effect 的一部分。
React 何時清除 effect?-- React 會在組件卸載的時候執行清除操作。正如之前學到的,effect 在每次渲染的時候都會執行。這就是為什么 React 會在執行當前 effect 之前對上一個 effect 進行清除。
(4)使用Effect 的提示:
i、使用多個 Effect 實現關注點分離:
使用 Hook 其中一個目的就是要解決 class 中生命周期函數經常包含不相關的邏輯,但又把相關邏輯分離到了幾個不同方法中的問題。
上面hook,effect事例就是使用多個effect的例子。
Hook 允許我們按照代碼的用途分離他們, 而不是像生命周期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的每一個 effect。
ii、為什么每次更新的時候都要運行 Effect:
並不需要特定的代碼來處理更新邏輯(class中的 componentDidUpdate),因為 useEffect 默認就會處理。它會在調用一個新的 effect 之前對前一個 effect 進行清理。
🌟iii、通過跳過 Effect 進行性能優化:
這是很常見的需求,所以它被內置到了 useEffect 的 Hook API 中。如果某些特定值在兩次重渲染之間沒有發生變化,你可以通知 React 跳過對 effect 的調用,只要傳遞數組作為 useEffect 的第二個可選參數即可:

如果 count 的值是 5,而且我們的組件重渲染的時候 count 還是等於 5,React 將對前一次渲染的 [5] 和后一次渲染的 [5] 進行比較。因為數組中的所有元素都是相等的(5 === 5),React 會跳過這個 effect,這就實現了性能的優化。
對於清除操作的effect(return 即清除)同樣:

如果你要使用此優化方式,請確保數組中包含了所有外部作用域中會隨時間變化並且在 effect 中使用的變量,否則你的代碼會引用到先前渲染中的舊變量。
如果想執行只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([])作為第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,所以它永遠都不需要重復執行。請記得 React 會等待瀏覽器完成畫面渲染之后才會延遲調用 useEffect,因此會使得額外操作很方便。
四、Hook 使用規則:
Hook 就是 JavaScript 函數,但是使用它們會有兩個額外的規則:
- 只能在函數最外層調用 Hook。不要在循環、條件判斷或者子函數中調用。
- 只能在 React 的函數組件中調用 Hook。不要在其他 JavaScript 函數中調用。(還有一個地方可以調用 Hook —— 就是自定義的 Hook 中,我們稍后會學習到。)
同時,我們提供了 linter 插件來自動執行這些規則。這些規則乍看起來會有一些限制和令人困惑,但是要讓 Hook 正常工作,它們至關重要。
五、自定義 Hook:
上面訂閱好友在線狀態功能,可以提取出來成為一個自定義Hook,提供給不同的組件使用。由於Hook 是一種復用狀態邏輯的方式,它不復用 state 本身。事實上 Hook 的每次調用都有一個完全獨立的 state —— 因此你可以在單個組件中多次調用同一個自定義 Hook。


約束:自定義 Hook 更像是一種約定而不是功能。如果函數的名字以 “use” 開頭並調用其他 Hook,我們就說這是一個自定義 Hook。 useSomething 的命名約定可以讓我們的 linter 插件在使用 Hook 的代碼中找到 bug。
六、Hook API 索引
https://zh-hans.reactjs.org/docs/hooks-reference.html
七、Hooks FAQ
https://zh-hans.reactjs.org/docs/hooks-faq.html
