React Hooks 加持下的函數組件設計


有了 react Hooks 的加持,媽媽再也不用擔心函數組件記不住狀態

過去,react 中的函數組件都被稱為無狀態函數式組件(stateless functional component),這是因為函數組件沒有辦法擁有自己的狀態,只能根據 Props 來渲染 UI ,其性質就相當於是類組件中的 render 函數,雖然結構簡單明了,但是作用有限。

但自從 React Hooks 橫空出世,函數組件也擁有了保存狀態的能力,而且也逐漸能夠覆蓋到類組件的應用場景,因此可以說 React Hooks 就是未來 React 發展的方向。

 

React Hooks 解決了什么問題

復雜的組件難以分拆

我們知道組件化的思想就是將一個復雜的頁面/大組件,按照不同層次,逐漸抽象並拆分成功能更純粹的小組件,這樣一方面可以減少代碼耦合,另外一方面也可以更好地復用代碼;但實際上,在使用 React 的類組件時,往往難以進一步分拆復雜的組件,這是因為邏輯是有狀態的,如果強行分拆,會令代碼復雜性急劇上升;如使用 HOC 和 Render Props 等設計模式,這會形成“嵌套地獄”,使我們的代碼變得晦澀難懂。

狀態邏輯復雜,給單元測試造成障礙

這其實也是上一點的延續:要給一個擁有眾多狀態邏輯的組件寫單元測試,無疑是一件令人崩潰的事情,因為需要編寫大量的測試用例來覆蓋代碼執行路徑。

組件生命周期繁復

對於類組件,我們需要在組件提供的生命周期鈎子中處理狀態的初始化、數據獲取、數據更新等操作,處理起來本身邏輯就比較復雜,而且各種“副作用”混在一起也使人頭暈目眩,另外還很可能忘記在組件狀態變更/組件銷毀時消除副作用。

React Hooks 就是來解決以上這些問題的

  • 針對狀態邏輯分拆復用難的問題:其實並不是 React Hooks 解決的,函數這一形式本身就具有邏輯簡單、易復用等特性。
  • 針對組件生命周期繁復的問題:React Hooks 屏蔽了生命周期這一概念,一切的邏輯都是由狀態驅動,或者說由數據驅動的,那么理解、處理起來就簡單多了。
 

利用自定義 Hooks 捆綁封裝邏輯與相關 state

我認為 React Hooks 的亮點不在於 React 官方提供的那些 API ,那些 API 只是一些基礎的能力;其亮點還是在於自定義 Hooks —— 一種封裝復用的設計模式。

例如,一個頁面上往往有很多狀態,這些狀態分別有各自的處理邏輯,如果用類組件的話,這些狀態和邏輯都會混在一起,不夠直觀:

class Com extends React.Component { state = { a: 1, b: 2, c: 3, } componentDidMount() { handleA() handleB() handleC() } }

而使用 React Hooks 后,我們可以把狀態和邏輯關聯起來,分拆成多個自定義 Hooks ,代碼結構就會更清晰:

function useA() { const [a, setA] = useState(1) useEffect(() => { handleA() }, []) return a } function useB() { const [b, setB] = useState(2) useEffect(() => { handleB() }, []) return b } function useC() { const [c, setC] = useState(3) useEffect(() => { handleC() }, []) return c } function Com() { const a = useA() const b = useB() const c = useC() }

我們除了可以利用自定義 Hooks 來拆分業務邏輯外,還可以拆分成復用價值更高的通用邏輯,比如說目前比較流行的 Hooks 庫:react-use;另外,React 生態中原來的很多庫,也開始提供 Hooks API ,如 react-router 。

 

忘記組件生命周期吧

React 提供了大量的組件生命周期鈎子,雖然在日常業務開發中,用到的不多,但光是 componentDidUpdate 和 componentWillUnmount 就讓人很頭痛了,一不留神就忘記處理 props 更新組件銷毀需要處理副作用的場景,這不僅會留下肉眼可見的 bug ,還會留下一些內存泄露的隱患。

類 MVVM 框架講究的是數據驅動,而生命周期這種設計模式,就明顯更偏向於傳統的事件驅動模型;當我們引入 React Hooks 后,數據驅動的特性能夠變得更純粹。

處理 props 更新

下面我們以一個非常典型的列表頁面來舉個例子:

class List extends Component { state = { data: [] } fetchData = (id, authorId) => { // 請求接口 } componentDidMount() { this.fetchData(this.props.id, this.props.authorId) // ...其它不相關的初始化邏輯 } componentDidUpdate(prevProps) { if ( this.props.id !== prevProps.id || this.props.authorId !== prevProps.authorId // 別漏了! ) { this.fetchData(this.props.id, this.props.authorId) } // ...其它不相關的更新邏輯 } render() { // ... } }

上面這段代碼有3個問題:

  • 需要同時在兩個生命周期里執行幾乎相同的邏輯。
  • 在判斷是否需要更新數據的時候,容易漏掉依賴的條件。
  • 每個生命周期鈎子里,會散落大量不相關的邏輯代碼,違反了高內聚的原則,影響閱讀代碼的連貫性。

如果改成用 React Hooks 來實現,問題就能得到很大程度上的解決了:

function List({ id, authorId }) { const [data, SetData] = useState([]) const fetchData = (id, authorId) => {} useEffect(() => { fetchData(id, authorId) }, [id, authorId]) }

改用 React Hooks 后:

  • 我們不需要考慮生命周期,我們只需要把邏輯依賴的狀態都丟進依賴列表里, React 會幫我們判斷什么時候該執行的。
  • React 官方提供了 eslint 的插件來檢查依賴項列表是否完整。
  • 我們可以使用多個 useEffect ,或者多個自定義 Hooks 來區分開多個無關聯的邏輯代碼段,保障高內聚特性。

處理副作用

最常見的副作用莫過於綁定 DOM 事件:

class List extends React.Component { handleFunc = () => {} componentDidMount() { window.addEventListener('scroll', this.handleFunc) } componentWillUnmount() { window.removeEventListener('scroll', this.handleFunc) } }

這塊也還是會有上述說的,影響高內聚的問題,改成 React Hooks :

function List() { useEffect(() => { window.addEventListener('scroll', this.handleFunc) }, () => { window.removeEventListener('scroll', this.handleFunc) }) }

而且比較絕的是,除了在組件銷毀的時候會觸發外,在依賴項變化的時候,也會執行清除上一輪的副作用。

 

利用 useMemo 做局部性能優化

在使用類組件的時候,我們需要利用 componentShouldUpdate 這個生命周期鈎子來判斷當前是否需要重新渲染,而改用 React Hooks 后,我們可以利用 useMemo 來判斷是否需要重新渲染,達到局部性能優化的效果:

function List(props) => { useEffect(() => { fetchData(props.id) }, [props.id]) return useMemo(() => ( // ... ), [props.id]) }

在上面這段代碼中,我們看到最終渲染的內容是依賴於props.id,那么只要props.id不變,即便其它 props 再怎么辦,該組件也不會重新渲染。

vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

依靠 useRef 擺脫閉包

在我們剛開始使用 React Hooks 的時候,經常會遇到這樣的場景:在某個事件回調中,需要根據當前狀態值來決定下一步執行什么操作;但我們發現事件回調中拿到的總是舊的狀態值,而不是最新狀態值,這是怎么回事呢?

function Counter() { const [count, setCount] = useState(0); const log = () => { setCount(count + 1); setTimeout(() => { console.log(count); }, 3000); }; return ( <button onClick={log}>報數</button> ); } /* 如果我們在三秒內連續點擊三次,那么count的值最終會變成 3,而隨之而來的輸出結果是? 0 1 2 */ 

“這是 feature 不是 bug ”,哈哈哈,說是 feature 可能也不太准確,因為這不正是 JavaScript 閉包的特性嗎?當我們每次往setTimeout里傳入回調函數時,這個回調函數都會引用下當前函數作用域(此時 count 的值還未被更新),所以在執行的時候打印出來的就會是舊的狀態值。

類組件是怎么實現的?

那為啥類組件中,每次都能取到最新的狀態值呢?這是因為我們在類組件中取狀態值都是從this.state里取的,這相當於是類組件的一個執行上下文,永遠都是保持最新的。

借助 useRef 共享修改

通過useRef創建的對象,其值只有一份,而且在所有 Rerender 之間共享

聽上去,這 useRef 其實跟 this.state 很相似嘛,都是一個可以一直維持的值,那我們就可以用它來維護我們的狀態了:

function Counter() { const count = useRef(0); const log = () => { count.current++; setTimeout(() => { console.log(count.current); }, 3000); }; return ( <button onClick={log}>報數</button> ); } /* 3 3 3 */


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM