摘要:當組件安裝和更新時,回調函數都會被調用。好在為我們提供了第二個參數,如果第二個參數傳入一個數組,僅當重新渲染時數組中的值發生改變時,中的回調函數才會執行。
前言
首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵,希望大家多多關注呀!React 16.8中新增了Hooks特性,并且在React官方文檔中新增加了Hooks模塊介紹新特性,可見React對Hooks的重視程度,如果你還不清楚Hooks是什么,強烈建議你了解一下,畢竟這可能真的是React未來的發展方向。
React一直以來有兩種創建組件的方式: Function Components(函數組件)與Class Components(類組件)。函數組件只是一個普通的JavaScript函數,接受props對象并返回React Element。在我看來,函數組件更符合React的思想,數據驅動視圖,不含有任何的副作用和狀態。在應用程序中,一般只有非?;A的組件才會使用函數組件,并且你會發現隨著業務的增長和變化,組件內部可能必須要包含狀態和其他副作用,因此你不得不將之前的函數組件改寫為類組件。但事情往往并沒有這么簡單,類組件也沒有我們想象的那么美好,除了徒增工作量之外,還存在其他種種的問題。
首先類組件共用狀態邏輯非常麻煩。比如我們借用官方文檔中的一個場景,FriendStatus組件用來顯示朋友列表中該用戶是否在線。
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return "Loading..."; } return this.state.isOnline ? "Online" : "Offline"; } }
上面FriendStatus組件會在創建時主動訂閱用戶狀態,并在卸載時會退訂狀態防止造成內存泄露。假設又出現了一個組件也需要去訂閱用戶在線狀態,如果想用復用該邏輯,我們一般會使用render props和高階組件來實現狀態邏輯的復用。
// 采用render props的方式復用狀態邏輯 class OnlineStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { const {isOnline } = this.state; return this.props.children({isOnline}) } } class FriendStatus extends React.Component{ render(){ return ({ ({isOnline}) => { if (isOnline === null) { return "Loading..."; } return isOnline ? "Online" : "Offline"; } } ); } }
// 采用高階組件的方式復用狀態邏輯 function withSubscription(WrappedComponent) { return class extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { return} } } const FriendStatus = withSubscription(({isOnline}) => { if (isOnline === null) { return "Loading..."; } return isOnline ? "Online" : "Offline"; })
上面兩種復用狀態邏輯的方式不僅需要費時費力地重構組件,而且Devtools查看組件的層次結構時,會發現組件層級結構變深,當復用的狀態邏輯過多時,也會陷入組件嵌套地獄(wrapper hell)的情況??梢娚鲜鰞煞N方式并不能完美解決狀態邏輯復用的問題。
不僅如此,隨著類組件中業務邏輯逐漸復雜,維護難度也會逐步提升,因為狀態邏輯會被分割到不同的生命周期函數中,例如訂閱狀態邏輯位于componentDidMount,取消訂閱邏輯位于componentWillUnmount中,相關邏輯的代碼相互割裂,而邏輯不相關的代碼反而有可能集中在一起,整體都是不利于維護的。并且相比如函數式組件,類組件學習更為復雜,你需要時刻提防this在組件中的陷阱,永遠不能忘了為事件處理程序綁定this。如此種種,看來函數組件還是有特有的優勢的。
Hooks函數式組件一直以來都缺乏類組件諸如狀態、生命周期等種種特性,而Hooks的出現就是讓函數式組件擁有類組件的特性。官方定義:
Hooks are functions that let you “hook into” React state and lifecycle features from function components.
要讓函數組件擁有類組件的特性,首先就要實現狀態state的邏輯。
State: useState useReduceruseState就是React提供最基礎、最常用的Hook,主要用來定義本地狀態,我們以一個最簡單的計數器為例:
import React, { useState } from "react" function Example() { const [count, setCount] = useState(0); return ({count}); }
useState可以用來定義一個狀態,與state不同的是,狀態不僅僅可以是對象,而且可以是基礎類型值,例如上面的Number類型的變量。useState返回的是一個數組,第一個是當前狀態的實際值,第二個用于更改該狀態的函數,類似于setState。更新函數與setState相同的是都可以接受值和函數兩種類型的參數,與useState不同的是,更新函數會將狀態替換(replace)而不是合并(merge)。
函數組件中如果存在多個狀態,既可以通過一個useState聲明對象類型的狀態,也可以通過useState多次聲明狀態。
// 聲明對象類型的狀態 const [count, setCount] = useState({ count1: 0, count2: 0 }); // 多次聲明 const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0);
相比于聲明對象類型的狀態,明顯多次聲明狀態的方式更加方便,主要是因為更新函數是采用的替換的方式,因此你必須給參數中添加未變化的屬性,非常的麻煩。需要注意的是,React是通過Hook調用的次序來記錄各個內部狀態的,因此Hook不能在條件語句(如if)或者循環語句中調用,并在需要注意的是,我們僅可以在函數組件中調用Hook,不能在組件和普通函數中(除自定義Hook)調用Hook。
當我們要在函數組件中處理復雜多層數據邏輯時,使用useState就開始力不從心,值得慶幸的是,React為我們提供了useReducer來處理函數組件中復雜狀態邏輯。如果你使用過Redux,那么useReducer可謂是非常的親切,讓我們用useReducer重寫之前的計數器例子:
import React, { useReducer } from "react" const reducer = function (state, action) { switch (action.type) { case "increment": return { count : state.count + 1}; case "decrement": return { count: state.count - 1}; default: return { count: state.count } } } function Example() { const [state, dispatch] = useReducer(reducer, { count: 0 }); const {count} = state; return ({count}); }
useReducer接受兩個參數: reducer函數和默認值,并返回當前狀態state和dispatch函數的數組,其邏輯與Redux基本一致。useReducer和Redux的區別在于默認值,Redux的默認值是通過給reducer函數賦值默認參數的方式給定,例如:
// Redux的默認值邏輯 const reducer = function (state = { count: 0 }, action) { switch (action.type) { case "increment": return { count : state.count + 1}; case "decrement": return { count: state.count - 1}; default: return { count: state.count } } }
useReducer之所以沒有采用Redux的邏輯是因為React認為state的默認值可能是來自于函數組件的props,例如:
function Example({initialState = 0}) { const [state, dispatch] = useReducer(reducer, { count: initialState }); // 省略... }
這樣就能實現通過傳遞props來決定state的默認值,當然React雖然不推薦Redux的默認值方式,但也允許你類似Redux的方式去賦值默認值。這就要接觸useReducer的第三個參數: initialization。
顧名思義,第三個參數initialization是用來初始化狀態,當useReducer初始化狀態時,會將第二個參數initialState傳遞initialization函數,initialState函數返回的值就是state的初始狀態,這也就允許在reducer外抽象出一個函數專門負責計算state的初始狀態。例如:
const initialization = (initialState) => ({ count: initialState }) function Example({initialState = 0}) { const [state, dispatch] = useReducer(reducer, initialState, initialization); // 省略... }
所以借助于initialization函數,我們就可以模擬Redux的初始值方式:
import React, { useReducer } from "react" const reducer = function (state = {count: 0}, action) { // 省略... } function Example({initialState = 0}) { const [state, dispatch] = useReducer(reducer, undefined, reducer()); // 省略... }Side Effects: useEffect useLayoutEffect
解決了函數組件中內部狀態的定義,接下來亟待解決的函數組件中生命周期函數的問題。在函數式思想的React中,生命周期函數是溝通函數式和命令式的橋梁,你可以在生命周期中執行相關的副作用(Side Effects),例如: 請求數據、操作DOM等。React提供了useEffect來處理副作用。例如:
import React, { useState, useEffect } from "react"; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times` return () => { console.log("clean up!") } }); return (); }You clicked {count} times
在上面的例子中我們給useEffect傳入了一個函數,并在函數內根據count值更新網頁標題。我們會發現每次組件更新時,useEffect中的回調函數都會被調用。因此我們可以認為useEffect是componentDidMount和componentDidUpdate結合體。當組件安裝(Mounted)和更新(Updated)時,回調函數都會被調用。觀察上面的例中,回調函數返回了一個函數,這個函數就是專門用來清除副作用,我們知道類似監聽事件的副作用在組件卸載時應該及時被清除,否則會造成內存泄露。清除函數會在每次組件重新渲染前調用,因此執行順序是:
render -> effect callback -> re-render -> clean callback -> effect callback
因此我們可以使用useEffect模擬componentDidMount、componentDidUpdate、componentWillUnmount行為。之前我們提到過,正是因為生命周期函數,我們迫不得已將相關的代碼拆分到不同的生命周期函數,反而將不相關的代碼放置在同一個生命周期函數,之所以會出現這個情況,主要問題在于我們并不是依據于業務邏輯書寫代碼,而是通過執行時間編碼。為了解決這個問題,我們可以通過創建多個Hook,將相關邏輯代碼放置在同一個Hook來解決上述問題:
import React, { useState, useEffect } from "react"; function Example() { useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); useEffect(() => { otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return function cleanup() { otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); // 省略... }
我們通過多個Hook來集中邏輯關注點,避免不相關的代碼糅雜而出現的邏輯混亂。但是隨之而來就遇到一個問題,假設我們的某個行為確定是要在區分componentDidUpdate或者componentDidMount時才執行,useEffect是否能區分。好在useEffect為我們提供了第二個參數,如果第二個參數傳入一個數組,僅當重新渲染時數組中的值發生改變時,useEffect中的回調函數才會執行。因此如果我們向其傳入一個空數組,則可以模擬生命周期componentDidMount。但是如果你想僅模擬componentDidUpdate,目前暫時未發現什么好的方法。
useEffect與類組件生命周期不同的是,componentDidUpdate和componentDidMount都是在DOM更新后同步執行的,但useEffect并不會在DOM更新后同步執行,也不會阻塞更新界面。如果需要模擬生命周期同步效果,則需要使用useLayoutEffect,其使用方法和useEffect相同,區域只在于執行時間上。
Context:useContext借助Hook:useContext,我們也可以在函數組件中使用context。相比于在類組件中需要通過render props的方式使用,useContext的使用則相當方便。
import { createContext } from "react" const ThemeContext = createContext({ color: "color", background: "black"}); function Example() { const theme = useContext(Conext); return (Hello World!
); } class App extends Component { state = { color: "red", background: "black" }; render() { return (); } }
useContext接受函數React.createContext返回的context對象作為參數,返回當前context中值。每當Provider中的值發生改變時,函數組件就會重新渲染,需要注意的是,即使的context的未使用的值發生改變時,函數組件也會重新渲染,正如上面的例子,Example組件中即使沒有使用過background,但background發生改變時,Example也會重新渲染。因此必要時,如果Example組件還含有子組件,你可能需要添加shouldComponentUpdate防止不必要的渲染浪費性能。
Ref: useRef useImperativeHandleuseRef常用在訪問子元素的實例:
function Example() { const inputEl = useRef(); const onButtonClick = () => { inputEl.current.focus(); }; return ( <> > ); }
上面我們說了useRef常用在ref屬性上,實際上useRef的作用不止于此
const refContainer = useRef(initialValue)
useRef可以接受一個默認值,并返回一個含有current屬性的可變對象,該可變對象會將持續整個組件的生命周期。因此可以將其當做類組件的屬性一樣使用。
useImperativeHandle用于自定義暴露給父組件的ref屬性。需要配合forwardRef一起使用。
function Example(props, ref) { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return ; } export default forwardRef(Example);
class App extends Component { constructor(props){ super(props); this.inputRef = createRef() } render() { return ( <>New Feature: useCallback useMemo> ); } }
熟悉React的同學見過類似的場景:
class Example extends React.PureComponent{ render(){ // ...... } } class App extends Component{ render(){ returnthis.setState()}/> } }
其實在這種場景下,雖然Example繼承了PureComponent,但實際上并不能夠優化性能,原因在于每次App組件傳入的onChange屬性都是一個新的函數實例,因此每次Example都會重新渲染。一般我們為了解決這個情況,一般會采用下面的方法:
class App extends Component{ constructor(props){ super(props); this.onChange = this.onChange.bind(this); } render(){ return} onChange(){ // ... } }
通過上面的方法一并解決了兩個問題,首先保證了每次渲染時傳給Example組件的onChange屬性都是同一個函數實例,并且解決了回調函數this的綁定。那么如何解決函數組件中存在的該問題呢?React提供useCallback函數,對事件句柄進行緩存。
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
useCallback接受函數和一個數組輸入,并返回的一個緩存版本的回調函數,僅當重新渲染時數組中的值發生改變時,才會返回新的函數實例,這也就解決我們上面提到的優化子組件性能的問題,并且也不會有上面繁瑣的步驟。
與useCallback類似的是,useMemo返回的是一個緩存的值。
const memoizedValue = useMemo( () => complexComputed(), [a, b], );
也就是僅當重新渲染時數組中的值發生改變時,回調函數才會重新計算緩存數據,這可以使得我們避免在每次重新渲染時都進行復雜的數據計算。因此我們可以認為:
useCallback(fn, input) 等同于 useMemo(() => fn, input)
如果沒有給useMemo傳入第二個參數,則useMemo僅會在收到新的函數實例時,才重新計算,需要注意的是,React官方文檔提示我們,useMemo僅可以作為一種優化性能的手段,不能當做語義上的保證,這就是說,也會React在某些情況下,即使數組中的數據未發生改變,也會重新執行。
自定義Hook我們前面講過,Hook只能在函數組件的頂部調用,不能再循環、條件、普通函數中使用。我們前面講過,類組件想要共享狀態邏輯非常麻煩,必須要借助于render props和HOC,非常的繁瑣。相比于次,React允許我們創建自定義Hook來封裝共享狀態邏輯。所謂的自定義Hook是指以函數名以use開頭并調用其他Hook的函數。我們用自定義Hook來重寫剛開始的訂閱用戶狀態的例子:
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(isOnline) { setIsOnline(isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } function FriendStatus() { const isOnline = useFriendStatus(); if (isOnline === null) { return "Loading..."; } return isOnline ? "Online" : "Offline"; }
我們用自定義Hook重寫了之前的訂閱用戶在線狀態的例子,相比于render prop和HOC復雜的邏輯,自定義Hook更加的簡潔,不僅于此,自定義Hook也不會引起之前我們說提到過的組件嵌套地獄(wrapper hell)的情況。優雅的解決了之前類組件復用狀態邏輯困難的情況。
總結 借助于Hooks,函數組件已經能基本實現絕大部分的類組件的功能,不僅于此,Hooks在共享狀態邏輯、提高組件可維護性上有具有一定的優勢??梢灶A見的是,Hooks很有可能是React可預見未來大的方向。React官方對Hook采用的是逐步采用策略(Gradual Adoption Strategy),并表示目前沒有計劃會將class從React中剔除,可見Hooks會很長時間內和我們的現有代碼并行工作,React并不建議我們全部用Hooks重寫之前的類組件,而是建議我們在新的組件或者非關鍵性組件中使用Hooks。
如有表述不周之處,虛心接受批評指教。愿大家一同進步!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102305.html
摘要:組件的職責增長并變得不可分割。是架構的重要組成部分。有許多好處,但它們為初學者創造了入門的障礙。方法使用狀態鉤子的最好方法是對其進行解構并設置原始值。第一個參數將用于存儲狀態,第二個參數用于更新狀態。 學習目標 在本文結束時,您將能夠回答以下問題: 什么是 hooks? 如何使用hooks? 使用hooks的一些規則? 什么是custom hook(自定義鉤子)? 什么時候應該使用 ...
摘要:前言樓主最近在整理的一些資料,為項目重構作準備,下午整理成了這篇文章。給傳入的是一個初始值,比如,這個按鈕的最初要顯示的是。取代了提供了一個統一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...
摘要:前言樓主最近在整理的一些資料,為項目重構作準備,下午整理成了這篇文章。給傳入的是一個初始值,比如,這個按鈕的最初要顯示的是。取代了提供了一個統一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...
摘要:課程制作和案例制作都經過精心編排。對于開發者意義重大,希望對有需要的開發者有所幫助。是從提案轉為正式加入的新特性。并不需要用繼承,而是推薦用嵌套。大型項目中模塊化與功能解耦困難。從而更加易于復用和獨立測試。但使用會減少這種幾率。 showImg(https://segmentfault.com/img/bVbpNRZ?w=1920&h=1080); 講師簡介 曾任職中軟軍隊事業部,參與...
摘要:到目前為止,表達這種流程的基本形式是課程。按鈕依次響應并更改獲取更新的文本。事實證明不能從返回一個??梢栽诮M件中使用本地狀態,而無需使用類。替換了提供統一,和。另一方面,跟蹤中的狀態變化確實很難。 備注:為了保證的可讀性,本文采用意譯而非直譯。 在這個 React鉤子 教程中,你將學習如何使用 React鉤子,它們是什么,以及我們為什么這樣做! showImg(https://segm...
閱讀 5200·2021-10-15 09:42
閱讀 1606·2021-09-22 16:05
閱讀 3261·2021-09-22 15:57
閱讀 3396·2019-12-27 12:06
閱讀 967·2019-08-29 15:16
閱讀 2880·2019-08-26 12:24
閱讀 380·2019-08-26 12:02
閱讀 1885·2019-08-23 16:00