hello~親愛的觀衆老爺們你們好~最近負責重構某個內部系統,既然是內部系統,那固然能夠盡情搞事情,分析需求後決定採用 React
最新版本進行重構。既然是最新的版本,那固然是使用 Hooks
進行開發了。開發的過程並不是一路順風,但也算是踩過很多坑也從新爬出來了,小結後有了這篇文章~javascript
注意~這篇文章是實踐相關的討論,若是不太清楚 Hooks
的同窗,可能這篇文章不太適合你。同時,我在實踐的過程當中,使用了若干 React
官方不建議的模式,本文主要是對此進行討論,但願對你編寫基於 Hooks
的 React
代碼時有所幫助~如下是正文:html
Hook
React
的官網在 Hook
規則 一節中,有這麼一段話:前端
不要在循環,條件或嵌套函數中調用
Hook
, 確保老是在你的React
函數的最頂層調用他們。java
然而,我在實際的開發過程當中,(爲了省事)常常違背了這條原則。先理解一下爲什麼官方不建議在循環,條件或嵌套函數中調用 Hook
。函數組件內的 Hook
是基於鏈表進行註冊的,也就是一個有着固定順序的序列,以下所示:react
Hook1 ⟶️ Hook2 ⟶️ Hook3 ...⟶️... HookN
複製代碼
假設 Hook2
處於判斷條件之中,一旦 condition
修改,執行的順序就會發生改變:數組
if (condition) {
useHook2();
}
執行順序變爲:Hook1 ⟶️ Hook3 ...⟶️... HookN
複製代碼
順序會發生偏移,從而致使 React
內部報錯。緩存
大體瞭解代碼原理後,就能理解爲什麼官方不建議在循環或條件中使用 Hook
。然而,這個問題的根本緣由是順序產生了變化,於是致使 bug 的出現。那若是咱們能保證執行順序呢?好比:在本地開發環境中須要增長一些調試代碼,但不但願線上出現對應的代碼,通常咱們會這麼寫:服務器
if (process.env.NODE_ENV === 'development') {
//do sth...
}
複製代碼
發佈正式代碼時,這段調試代碼會被打包工具正確去除。因爲環境變量是固定的(同一環境之中基本上不會產生變化),於是即便 Hook
處於條件判斷之中,函數中 Hook
的順序是固定的,使用是並不會產生問題:函數
function Test() {
let test;
let setTest;
if (process.env.NODE_ENV === 'development') {
[test, setTest] = useState(1); // eslint-disable-line
}
// 添加 eslint-disable-line,是由於 ESLint 在開發環境中會檢測 `Hook` 是否在合理的上下文之中(即不在條件判斷或循環之中),否則會報錯並中止渲染
return (
<div> <p>{test}</p> <button onClick={() => setTest(test + 1)}>click</button> </div>
)
}
複製代碼
在循環條件中同理(只要循環次數固定,也不會有問題)。只要肯定 Hook
的順序不變,何嘗不可在條件或循環中使用,只要你清楚知道本身在幹什麼~工具
不管是 React
的文檔仍是 Hooks
相關的文章,不少都建議咱們對 Hook
的依賴數組誠實,如實填寫 Hook
內的變量,以免 bug 的產生:
useEffect(() => {
reportToService(userName, userId, ...)
}, [userName, userId, ...])
複製代碼
然而,這並不應是死板的教條~想象一下上述代碼的場景,這是一段上報數據到服務器的邏輯。若是用戶登出後,用戶名之類的信息必然產生變化,那麼 useEffect
會從新執行,假設這是一段統計 PV 的代碼,那就存在重複統計的問題。於是,只要你清楚知道本身在幹什麼,依賴項實際上是能夠根據實際狀況填寫的,不必強行將所有用到的變量填進去。
而精確依賴,就是 React
不可變數據的一個體現,某程度上說,應該說是避免錯誤填寫依賴,考慮如下例子:
function Test() {
const [test, setTest] = useState({
key1: 1,
key2: 2
});
useEffect(() => {
console.log(test.key2);
}, [test]);
return (
<div> <p>{test.key1}</p> <button onClick={() => setTest(test => { return { ...test, key1: test.key1 + 1 } })}> click </button> </div>
)
}
複製代碼
以上例子能夠正常運行,ESLint
也不會報錯。然而當咱們不斷點擊按鈕時,useEffect
會不斷執行,打印出 test.key2
的值。這是因爲按鈕點擊後, test
是一個新的對象,於是 useEffect
的依賴項發生了變化,因而不斷被執行。但這是毫無心義的,咱們真正想監聽的的是 test.key2
,於是依賴的項應該精準地填寫爲 test.key2
,修改後 useEffect
只會在 test.key2
變化後纔會執行。
精確依賴是一個很小的細節,但常常會致使重複執行 Hook
,在定位相似問題時不妨先檢查一下依賴了錯誤的變量。
Hook
既不是 setState
,也不是生命週期函數因爲我很長一段時間沒寫 React
,於是一開始寫 Hook
的時候仍是帶着濃重的 class
色彩,基本能夠說只是將 class
「翻譯」成 function
而已。
在使用 useState
的時候,往裏面丟一個很大的對象,模仿以前 state
的寫法,但這是典型的反模式。儘管數據的封裝是必須的,如將用戶相關的數據統合在 userInfo
對象之中,然而將所有內容像以前的 class
同樣,放在一個 state
中,是不可取的,會致使這個 Hook
變得至關繁重,也不利於邏輯複用,違背了 Hook
最初的目的。
Hook
不是 setState
仍是比較好理解,但 Hook
不是生命週期函數就不是那麼好習慣了。例如咱們常常在 componentWillUnmount
中解除定時器、解綁事件等等:
componentWillUnmount() {
clearTimeout(timer);
removeEventListener('click', handler);
...
}
複製代碼
然而,在 Hook
中,把解除定時器、解綁事件等操做所有寫到一個 useEffect
中去,並非最佳實踐。經過上文咱們瞭解到,儘可能要作到精確依賴,避免沒必要要的開銷。而從邏輯複用的角度而言,將代碼按照功能拆開,更有利於複用。於是咱們應該寫成:
useEffect(() => {
const timer = setTimeout(() => {
...
});
return () => {
clearTimeout(timer);
}
}, []);
useEffect(() => {
element.addEventListener('click', handler);
return () => {
element.removeEventListener('click', handler);
}
}, []);
...
複製代碼
class
中 每一個生命週期函數只有一個,而 function
中相應的 Hooks
能夠有多個~分拆有利於代碼清晰與邏輯複用。
性能是個很大的話題,在 Hooks
中性能相關的問題,基本能夠獨立一篇文章來寫了~因爲 React
遵循的是不可變數據,所以它的更新是批量的:
按照最佳實踐,咱們應該使用 useMemo
、 useCallback
等等進行緩存,避免重複生成變量而致使無心義的重算 Virtual DOM。但喜歡「搗亂」的我,稍微提出一點反模式以作拋磚引玉之用——咱們的應用可能沒那麼大以致於須要全面考慮性能。
使用 Hook
進行開發時,爲了保證變量不變,須要使用不少 useMemo
、 useCallback
之類的鉤子,爲了極致的性能每一個變量與方法都要被這些鉤子包囊。有時候感受就像是不斷地書寫模板代碼,回憶起以前被 Redux
支配的恐懼了麼~ Hook
也有點這個感受。
要明確一點,性能確實很重要。但也要明白,對於通常的前端應用而言,在 1ms 仍是 10ms 內完成 diff
,其實意義是不大的。按照經驗而言,低端手機百萬級別內的運算,並不會產生明顯的卡頓。然而 DOM
就至關慢了,低端機大概 6000 個 DOM
從新渲染,就會產生很是明顯的卡頓。於是,就我的的角度而言,保持應用的高性能值得稱道,但不建議在中小的應用中追求極致性能,在碰到性能瓶頸時再進行優化也何嘗不可。
以上就是本文的所有內容啦!這是我在項目中使用 Hook
進行開發後的一點思考,畢竟「盡信書,不如無書」,只有經過實踐才能掌握新的技術。
以上是我的的一點淺見,感謝各位看官大人看到這裏。知易行難,但願本文對你有所幫助~謝謝!