有了 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 */
