學(xué)習(xí) React Hooks

B站不允許外鏈的超鏈接,只能直接貼出來了??。

React Hook 或許是 React 中最有趣的玩意了,相較于隔壁 vue 的“競(jìng)品” setup,hook 的行為和普通函數(shù)的行為非常相像(我覺得這是它最棒的一點(diǎn)——對(duì)函數(shù)的心智模型可以被繼續(xù)沿用),且更容易學(xué)習(xí)(vue 里一萬個(gè)概念能夠把你繞暈十次甚至九次),更容易避免引用泄漏相關(guān)的 bug。最近進(jìn)行了很多學(xué)習(xí),是時(shí)候該進(jìn)行一個(gè)筆記的做了。
perspective
理解 Hook 的運(yùn)行原理的關(guān)鍵在于:對(duì)于使用 Hook 的函數(shù)式組件,它的每一次執(zhí)行都有著自己的 props 和 state,且對(duì)于每一次執(zhí)行,它的 props 和 state 對(duì)這一趟而言都是常量。這意味著什么呢?這意味著我們?cè)谒伎计溥\(yùn)行機(jī)制時(shí),可以使用所謂的代換模型——把 props 和 state 用它的值去替代。
容易發(fā)現(xiàn),這和傳遞值類型給函數(shù)的行為是一致的
代換模型實(shí)際上只對(duì) props,state 等生效,對(duì)于 setState,useRef,dispatch 等是無效的——它們?cè)诙啻螆?zhí)行中保持的都是同一個(gè)引用,但我認(rèn)為它仍有使用的價(jià)值。
考慮一個(gè)最經(jīng)典的例子。
會(huì)發(fā)生什么呢?其實(shí)即使沒學(xué)過 hook,只要學(xué)過閉包就能得到答案了——counter 將始終為 1,這是因?yàn)閷?duì)于這一趟的執(zhí)行,它看到的 counter 為常量 0,我們可以用代換模型去描述:
That’s it!這個(gè)組件在之后或許還會(huì)被渲染數(shù)次(代碼中沒體現(xiàn)),但這個(gè) interval
函數(shù)看到的永遠(yuǎn)是這一趟的 counter,即 0。這個(gè)問題有數(shù)種解決方案,比如在依賴數(shù)組中加入 counter(這樣 counter
每次改變時(shí)舊的 interval 會(huì)被銷毀,新的計(jì)數(shù)器會(huì)被創(chuàng)建,看著有點(diǎn)怪,但確實(shí)有用),或者使用傳遞函數(shù)的 setState,即setCounter(counter => counter + 1)
,也可以使用 useRef(雖然它并沒意義用于此,且會(huì)引入新的復(fù)雜度,因?yàn)?useRef 的值的改變不會(huì)自動(dòng)觸發(fā)重渲染)。
順便,setState 的函數(shù)式更新,即setCounter(counter => counter + 1)
這種形式是同步的,這意味著可以通過某些方式把這時(shí)候的 counter 取出來,從而避免上面的問題:
但在實(shí)踐中絕不應(yīng)該使用這個(gè)操作:它是“未定義”的,在不同的版本中可能會(huì)有不同的表現(xiàn),這里只是隨便提一嘴。
另外,對(duì)于 useEffect,不應(yīng)當(dāng)考慮其為類組件生命周期的模擬,而是認(rèn)為它是一種將 props 和 state 和 DOM 之外的事物進(jìn)行同步的手段。下面該挨個(gè)過堂。
useState
useState 即創(chuàng)建一個(gè)狀態(tài),它其實(shí)并沒有什么可說的。下面是一個(gè)示例,這里將 useState 的第一個(gè)返回值稱為 state,第二個(gè)稱為 setState。
useState 的行為是被明確說明了的:第一次渲染時(shí),其會(huì)被賦予初始值即 0,之后通過 setCount 去修改它的值。
這里有一個(gè)有趣的地方在于,函數(shù)式組件的每一次渲染都是一次普通的函數(shù)調(diào)用,因此這里的useState<number>(0)
也只是一次普通的函數(shù)調(diào)用(其它 hook 亦然)。
既然是普通的函數(shù)調(diào)用,那它的計(jì)算模型當(dāng)然和普通的函數(shù)一致——先計(jì)算參數(shù)值,再傳給函數(shù)去求值,因此這里的 0 每次都會(huì)被求值。在這里這并非是個(gè)問題,但倘若求初始值是一個(gè)很昂貴的操作呢?比如從 localStorage 中取一個(gè)值?倘若仍舊按原來的寫法,那每一次渲染都會(huì)去拿一次 localStorage。性能被無意義地?fù)p耗了。
React 顯然知道這個(gè)問題,因此它允許通過一個(gè)函數(shù)去創(chuàng)建初始值,這個(gè)函數(shù)顯然只會(huì)被 React 調(diào)用一次,這顯然運(yùn)用了惰性求值/傳名調(diào)用(call by name)——隨你怎么說——的模式:
useState 另一個(gè)需要關(guān)注的地方在于,state 在每一趟調(diào)用中是不同的——每一趟渲染中看到的 state 都是這一趟看到的,而 setState 在每一趟中都是相同的,因此 setState 不需要放在 useEffect 等的依賴數(shù)組中。
useEffect
useEffect,給予函數(shù)式組件制造副作用的能力。
useEffect 的行為和 useState 的很類似:每一趟渲染時(shí),這一趟的 effect 看到的是這一趟的 props 和 state,這等于是說,每次渲染都有它自己的 Effects,考慮下面的代碼:
在這 5 秒內(nèi),無論我們點(diǎn)擊多少次按鈕,最后輸出的永遠(yuǎn)是 0,因?yàn)檫@一次 effect 的執(zhí)行看到的僅是這一次的 count。
同步,而非生命周期
這篇文章 https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/#%E5%90%8C%E6%AD%A5%EF%BC%8C-%E8%80%8C%E9%9D%9E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F 的這一節(jié)《同步,而非生命周期》 我認(rèn)為很好地詮釋了對(duì) Effect 的心智模型:React 根據(jù)把組件當(dāng)前的 props 和 state 同步給 DOM,而 Effect 則是根據(jù) props 和 state 同步 DOM 以外的東西。在這個(gè)心智模型中,不存在所謂的生命周期,只有 props 和 state 到特定事物的一個(gè)映射,我們關(guān)注目的,而非過程。容易類比,這也是聲明式編程相較于命令式編程,react 相較于 jquery 的不同:關(guān)注要做什么(目的),而非如何做(過程)。
比如,我們可以把網(wǎng)頁的 title 和組件的一個(gè) state 去同步:
但這種同步并非是單向的,React 也需要接受來自 DOM(以及其他地方)的事件去更新 state,Effect(以及事件處理器)有時(shí)候也是依據(jù) DOM 之外的其它東西去修改 state,比如 HTTP 請(qǐng)求,下面是來自于 這篇文章 https://www.robinwieruch.de/react-hooks-fetch-data/ 的一個(gè)比較復(fù)雜的例子,它通過請(qǐng)求來更新 state。
這段代碼用這里的心智模型怎么理解呢?我們關(guān)注目的,所以我們說我們將這個(gè) http 接口(以及 query 這個(gè)狀態(tài))和 data 這個(gè) state 進(jìn)行同步。
顯然,這種同步和這個(gè)接口的狀態(tài)以及 query 狀態(tài)相關(guān),每當(dāng) query 改變,或者接口狀態(tài)改變,data 這個(gè) state 也應(yīng)當(dāng)進(jìn)行改變;但后者顯然是 react 監(jiān)視不到的,或者說,后者的變化無法觸發(fā)組件的重新渲染(似乎只有 setState,setReducer 以及某些不可名狀的其它情況會(huì)觸發(fā)重新渲染)。
這里會(huì)有另一個(gè)有趣的問題——從遠(yuǎn)程獲取數(shù)據(jù)時(shí),是在事件監(jiān)聽器去直接獲取,還是去修改相應(yīng)狀態(tài),在 useEffect 中去獲???顯然后者是更加接近這里的心智模型的,但究竟孰優(yōu)孰劣?亦或是兩者都有適用場(chǎng)景?
關(guān)于清理函數(shù)
要理解清理函數(shù)的行為,必須理解 Effect 的執(zhí)行流程。Effect 在每次渲染之后執(zhí)行,在這一次的 Effect 執(zhí)行之前,會(huì)先執(zhí)行上一次 Effect 的清理函數(shù),順序?yàn)椋?/p>
渲染完成
上一次的清理函數(shù)執(zhí)行
這一次的 Effect 執(zhí)行
這里說的是“次”而非“趟”——只有這個(gè)Effect確實(shí)執(zhí)行了才算數(shù),比如下面這個(gè) Effect 的清理函數(shù)只在組件 unmount 時(shí)才執(zhí)行。
比如下面這個(gè) Effect 的清理函數(shù)只在重渲染且 data 這個(gè)變量改變時(shí)才執(zhí)行:
考慮這樣一個(gè)場(chǎng)景,組件有一個(gè) id prop,我們希望用這個(gè) id 去訂閱某個(gè)聊天室,每次 id 改變時(shí),我們都希望取消之前的訂閱,再訂閱新的聊天室,這個(gè)怎么實(shí)現(xiàn)呢?這個(gè)問題來自于 React FAQ https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state,這篇文章 https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/#%E9%82%A3effect%E4%B8%AD%E7%9A%84%E6%B8%85%E7%90%86%E5%8F%88%E6%98%AF%E6%80%8E%E6%A0%B7%E7%9A%84%E5%91%A2%EF%BC%9F 也有描述。
實(shí)際上什么都不需要做:
這仍舊可以用代換模型來解決,比如 id 從 10 變成 20,兩次的代碼是這樣:
上一趟的清理函數(shù)看到的是上一趟的 id,這一趟看到的是這一趟的 id。
這里有另一個(gè)有趣的場(chǎng)景,假設(shè)在某個(gè) Effect 里執(zhí)行某個(gè)異步操作,而且這個(gè) Effect 連續(xù)執(zhí)行了兩次,而且舊的請(qǐng)求到達(dá)的更晚……如果沒有任何操作的話,則新的請(qǐng)求的結(jié)果會(huì)被舊的請(qǐng)求的結(jié)果去覆蓋掉,這不是我們想要的,為此我們要么去在執(zhí)行新的請(qǐng)求的時(shí)候銷毀舊的請(qǐng)求,要么讓它“失能”,后者的操作更加通用,可以歸結(jié)為一個(gè)模式:
這個(gè) Effect 若執(zhí)行了下一次,則這一次的清理函數(shù)會(huì)被調(diào)用,ignore 會(huì)設(shè)置為 true,因此這個(gè)請(qǐng)求的結(jié)果會(huì)被忽略。
關(guān)于依賴數(shù)組
useEffect 以及其它 hook 的依賴數(shù)組也是非常有趣的部分,它汗 useCallback,useMemo 等都可以認(rèn)為都是對(duì)上面所說的同步的優(yōu)化。
依賴數(shù)組的原理十分簡(jiǎn)單——每次重新渲染的時(shí)候,React 會(huì)比較當(dāng)前依賴數(shù)組中的值和上一次的值是否有修改(使用Object.is
,行為類似===
),若有修改才去觸發(fā) Effect。
React 的官方文檔以及其它文章都會(huì)警告你,說你應(yīng)當(dāng)在依賴數(shù)組中包含你依賴的所有 props,state 以及其它可能會(huì)有修改的量(包括在函數(shù)組件頂級(jí)作用域中定義的變量,函數(shù)),否則這是不安全的,這在某些情景下會(huì)讓人感覺很奇怪,但其實(shí)他們的意思是說,它的行為將可能會(huì)和你的預(yù)期不符。
這里相關(guān)的具體細(xì)節(jié)還是直接看相關(guān)的 文章?https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/#%E5%85%B3%E4%BA%8E%E4%BE%9D%E8%B5%96%E9%A1%B9%E4%B8%8D%E8%A6%81%E5%AF%B9react%E6%92%92%E8%B0%8E 更合適,但無論如何,持續(xù)維護(hù)依賴數(shù)組(并且安裝 react 提供的 lint 插件)總是個(gè)好主意。
useReducer
倘若對(duì)一個(gè)狀態(tài)的更新的邏輯依賴其他狀態(tài)的值,這可能就是用 useReducer 的時(shí)候了。useReducer 以純函數(shù)的形式統(tǒng)一了更新一系列狀態(tài)的接口,并枚舉出所有對(duì)這一系列狀態(tài)所做的動(dòng)作(action),以方便管理和維護(hù)復(fù)雜狀態(tài)。
在 Typescript 中使用 reducer 是非常舒服的,下面是一個(gè)簡(jiǎn)單的實(shí)例,其中 Action 中的 FIELD 事件表示對(duì)特定狀態(tài)進(jìn)行更新,這個(gè)操作過于通用,因此不應(yīng)當(dāng)使用(倘若某個(gè) reducer 提供這個(gè) action 還能正常作用,那估計(jì)根本沒必要去使用 reducer):
順帶一提,useReducer 還能這么玩:
我對(duì) useReducer 了解不多,就這樣了。十分好奇為何 useReducer 的初始化函數(shù)放到第三個(gè)參數(shù),沒有和 useState 去統(tǒng)一。
useCallback & useMemo
有時(shí)候,組件的狀態(tài)粒度過細(xì),無法直接利用,我們就會(huì)嘗試編寫函數(shù)和定義變量來先進(jìn)行一些操作,比如下面的實(shí)例:
第一印象還好,但有如下幾個(gè)問題:
它們每次重新渲染時(shí)都會(huì)被執(zhí)行,而執(zhí)行過程可能是昂貴的
倘若定義的是復(fù)雜對(duì)象或者函數(shù),則它們每次執(zhí)行時(shí)都會(huì)改變(使用 Object.is 不相等),因此無法加到依賴數(shù)組中
為此,React 提供了 useMemo 和 useCallback 兩個(gè) hook 來解決此問題。
useMemo 的性質(zhì)就像 vue 的計(jì)算屬性(但是是只讀的),它會(huì)緩存計(jì)算值,并只在它的依賴有改變的時(shí)候重新計(jì)算,未重新計(jì)算時(shí),拿到的值總是同一個(gè),即用 Object.is 能比較相等。
useCallback 不會(huì)緩存計(jì)算值,而是緩存計(jì)算的函數(shù),以保證在依賴項(xiàng)未改變時(shí)仍舊為同一個(gè)函數(shù),或者說使函數(shù)本身只在需要的時(shí)候才改變。若想在 Effect 中去引用定義在組件頂級(jí)作用域的函數(shù)且不違背依賴數(shù)組,必須使用 useCallback,見 這里 https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/#%E4%BD%86%E6%88%91%E4%B8%8D%E8%83%BD%E6%8A%8A%E8%BF%99%E4%B8%AA%E5%87%BD%E6%95%B0%E6%94%BE%E5%88%B0effect%E9%87%8C。
useRef & useImperactiveHandle
ref 在 React 中有兩種功能:
用于操作 DOM 元素
子組件暴露給父組件操作/值的方式,常用于自定義不受管組件時(shí)
函數(shù)式組件中則有第三種功能:
實(shí)現(xiàn)函數(shù)式組件的“實(shí)例變量”——對(duì)于每一次渲染,其值都引用同一個(gè)對(duì)象,這使歷史的渲染也能夠影響當(dāng)前的該對(duì)象
操作 DOM 是我們都比較熟悉的,想要暴露給父組件操作的話子組件需要使用 forwardRef 函數(shù)(HOC?)包裝,并且使用 useImperativeHandle 去注冊(cè)操作/值,下面是一個(gè)簡(jiǎn)單的示例,子組件 Counter 暴露給父組件操作以清空它的值:
參考資料
A Complete Guide to useEffect(有中文,強(qiáng)烈推薦) https://overreacted.io/a-complete-guide-to-useeffect/
Hooks API Reference https://reactjs.org/docs/hooks-reference.html
How to fetch data with React Hooks https://www.robinwieruch.de/react-hooks-fetch-data/
React useReducer Hook ultimate guide https://blog.logrocket.com/react-usereducer-hook-ultimate-guide/