最近在項目中基本上所有使用了React Hooks,歷史項目也用React Hooks重寫了一遍,相比於Class組件,React Hooks的優勢能夠一句話來歸納:就是簡單,在React hooks中沒有複雜的生命週期,沒有類組件中複雜的this指向,沒有相似於HOC,render props等複雜的組件複用模式等。本篇文章主要總結一下在React hooks工程實踐中的經驗。react
- React hooks中的渲染行爲
- React hooks中的性能優化
- React hooks中的狀態管理和通訊
原文首發至個人博客: https://github.com/forthealll...ios
理解React hooks的關鍵,就是要明白,hooks組件的每一次渲染都是獨立,每一次的render都是一個獨立的做用域,擁有本身的props和states、事件處理函數等。歸納來說:git
每一次的render都是一個互不相關的函數,擁有徹底獨立的函數做用域,執行該渲染函數,返回相應的渲染結果es6
而類組件則不一樣,類組件中的props和states在整個生命週期中都是指向最新的那次渲染.github
React hooks組件和類組件的在渲染行爲中的區別,看起來很繞,咱們能夠用圖來區別,redux
上圖表示在React hooks組件的渲染過程,從圖中能夠看出,react hooks組件的每一次渲染都是一個獨立的函數,會生成渲染區專屬的props和state. 接着來看類組件中的渲染行爲:數組
類組件中在渲染開始的時候會在類組件的構造函數中生成一個props和state,全部的渲染過程都是在一個渲染函數中進行的而且,每一次的渲染中都不會去生成新的state和props,而是將值賦值給最開始被初始化的this.props和this.state。緩存
理解了React hooks的渲染行爲,就指示了咱們如何在工程中使用。首先由於React hooks組件在每一次渲染的過程當中都會生成獨立的所用域,所以,在組件內部的子函數和變量等在每次生命的時候都會從新生成,所以咱們應該減小在React hooks組件內部聲明函數。性能優化
寫法一:app
function App() { const [counter, setCounter] = useState(0); function formatCounter(counterVal) { return `The counter value is ${counterVal}`; } return ( <div className="App"> <div>{formatCounter(counter)}</div> <button onClick={() => setCounter(prevState => ++prevState)}> Increment </button> </div> ); }
寫法二:
function formatCounter(counterVal) { return `The counter value is ${counterVal}`; } function App() { const [counter, setCounter] = useState(0); return ( <div className="App"> <div>{formatCounter(counter)}</div> <button onClick={()=>onClick(setCounter)}> Increment </button> </div> ); }
App組件是一個hooks組件,咱們知道了React hooks的渲染行爲,那麼寫法1在每次render的時候都會去從新聲明函數formatCounter,所以是不可取的。咱們推薦寫法二,若是函數與組件內的state和props無相關性,那麼能夠聲明在組件的外部。若是函數與組件內的state和props強相關性,那麼咱們下節會介紹useCallback和useMemo的方法。
React hooks中的state和props,在每次渲染的過程當中都是從新生成和獨立的,那麼咱們若是須要一個對象,從開始到一次次的render1 , render2, ...中都是不變的應該怎麼作呢。(這裏的不變是不會從新生成,是引用的地址不變的意思,其值能夠改變)
咱們可使用useRef,建立一個「常量」,該常量在組件的渲染期內始終指向同一個引用地址。
經過useRef,能夠實現不少功能,好比在某次渲染的時候,拿到前一次渲染中的state。
function App(){ const [count,setCount] = useState(0) const prevCount = usePrevious(count); return ( <div> <h1>Now: {count}, before: {prevCount}</h1> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
上述的例子中,咱們經過useRef()建立的ref對象,在整個usePrevious組件的週期內都是同一個對象,咱們能夠經過更新ref.current的值,來在App組件的渲染過程當中,記錄App組件渲染中前一次渲染的state.
這裏其實還有一個不容易理解的地方,咱們來看usePrevious:
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
這裏的疑問是:爲何當value改變的時候,返回的ref.current指向的是value改變以前的值?
也就是說:
爲何useEffect在return ref.current以後才執行?
爲了解釋這個問題,咱們來聊聊神奇的useEffect.
hooks組件的每一次渲染均可以當作一個個獨立的函數 render1,render2 ... rendern,那麼這些render函數之間是怎麼關聯的呢,還有上小節的問題,爲何在usePrevious中,useEffect在return ref.current以後才執行。帶着這兩個疑問咱們來看看在hooks組件中,最爲神奇的useEffect。
用一句話歸納就是:
每一渲染都會生成不一樣的render函數,而且每一次渲染經過useEffect會生成一個不一樣的Effects,Effects在每次渲染後聲效。
每次渲染除了生成不一樣的做用域外,若是該hooks組件中使用了useEffect,經過useEffect還會生成一個獨有的effects,該effects在渲染完成後生效。
舉例來講:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
上述的例子中,完成的邏輯是:
<p>You clicked 0 times</p>
<p>You clicked 1 times</p>
也就是說每次渲染render中,effect位於同步執行隊列的最後面,在dom更新或者函數返回後在執行。
咱們在來看usePrevious的例子:
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
由於useEffect的機制,在新的渲染過程當中,先返回ref.current再執行deps依賴更新ref.current,所以usePrevios老是返回上一次的值。
如今咱們知道,在一次渲染render中,有本身獨立的state,props,還有獨立的函數做用域,函數定義,effects等,實際上,在每次render渲染中,幾乎全部都是獨立的。咱們最後來看兩個例子:
(1)
function Counter() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { console.log(`You clicked ${count} times`); }, 3000); }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
(2)
function Counter() { const [count, setCount] = useState(0); setTimeout(() => { console.log(`You clicked ${count} times`); }, 3000); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
這兩個例子中,咱們在3內點擊5次Click me按鈕,那麼輸出的結果都是同樣的。
You clicked 0 times
You clicked 1 times
You clicked 2 times
You clicked 3 times
You clicked 4 times
You clicked 5 times
總而言之,每一次渲染的render,幾乎都是獨立和獨有的,除了useRef建立的對象外,其餘對象和函數都沒有相關性.
前面咱們講了React hooks中的渲染行爲,也初步
提到了說將與state和props無關的函數,聲明在hooks組件外面能夠提升組件的性能,減小每次在渲染中從新聲明該無關函數. 除此以外,React hooks還提供了useMemo和useCallback來優化組件的性能.
(1).useCallback
有些時候咱們必需要在hooks組件內定義函數或者方法,那麼推薦用useCallback緩存這個方法,當useCallback的依賴項不發生變化的時候,該函數在每次渲染的過程當中不須要從新聲明
useCallback接受兩個參數,第一個參數是要緩存的函數,第二個參數是一個數組,表示依賴項,當依賴項改變的時候會去從新聲明一個新的函數,不然就返回這個被緩存的函數.
function formatCounter(counterVal) { return `The counter value is ${counterVal}`; } function App(props) { const [counter, setCounter] = useState(0); const onClick = useCallback(()=>{ setCounter(props.count) },[props.count]); return ( <div className="App"> <div>{formatCounter(counter)}</div> <button onClick={onClick}> Increment </button> </div> ); }
上述例子咱們在第一章的例子基礎上增長了onClick方法,並緩存了這個方法,只有props中的count改變的時候才須要從新生成這個方法。
(2).useMemo
useMemo與useCallback大同小異,區別就是useMemo緩存的不是函數,緩存的是對象(能夠是jsx虛擬dom對象),一樣的當依賴項不變的時候就返回這個被緩存的對象,不然就從新生成一個新的對象。
爲了實現組件的性能優化,咱們推薦:
在react hooks組件中聲明的任何方法,或者任何對象都必需要包裹在useCallback或者useMemo中。
(3)useCallback,useMemo依賴項的比較方法
咱們來看看useCallback,useMemo的依賴項,在更新先後是怎麼比較的
import is from 'shared/objectIs'; function areHookInputsEqual( nextDeps: Array<mixed>, prevDeps: Array<mixed> | null, ) { if (prevDeps === null) { return false; } if (nextDeps.length !== prevDeps.length) { return false } for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; }
其中is方法的定義爲:
function is(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) ); } export default (typeof Object.is === 'function' ? Object.is : is);
這個is方法就是es6的Object.is的兼容性寫法,也就是說在useCallback和useMemo中的依賴項先後是經過Object.is來比較是否相同的,所以是淺比較。
react hooks中的局部狀態管理相比於類組件而言更加簡介,那麼若是咱們組件採用react hooks,那麼如何解決組件間的通訊問題。
最基礎的想法可能就是經過useContext來解決組件間的通訊問題。
好比:
function useCounter() { let [count, setCount] = useState(0) let decrement = () => setCount(count - 1) let increment = () => setCount(count + 1) return { count, decrement, increment } } let Counter = createContext(null) function CounterDisplay() { let counter = useContext(Counter) return ( <div> <button onClick={counter.decrement}>-</button> <p>You clicked {counter.count} times</p> <button onClick={counter.increment}>+</button> </div> ) } function App() { let counter = useCounter() return ( <Counter.Provider value={counter}> <CounterDisplay /> <CounterDisplay /> </Counter.Provider> ) }
在這個例子中經過createContext和useContext,能夠在App的子組件CounterDisplay中使用context,從而實現必定意義上的組件通訊。
此外,在useContext的基礎上,爲了其總體性,業界也有幾個比較簡單的封裝:
https://github.com/jamiebuild...
https://github.com/diegohaz/c...
可是其本質都沒有解決一個問題:
若是context太多,那麼如何維護這些context
也就是說在大量組件通訊的場景下,用context進行組件通訊代碼的可讀性不好。這個類組件的場景一致,context不是一個新的東西,雖然用了useContext減小了context的使用複雜度。
hooks組件間的通訊,一樣可使用redux來實現。也就是說:
在React hooks中,redux也有其存在的意義
在hooks中存在一個問題,由於不存在相似於react-redux中connect這個高階組件,來傳遞mapState和mapDispatch, 解決的方式是經過redux-react-hook或者react-redux的7.1 hooks版原本使用。
在redux-react-hook中提供了StoreContext、useDispatch和useMappedState來操做redux中的store,好比定義mapState和mapDispatch的方式爲:
import {StoreContext} from 'redux-react-hook'; ReactDOM.render( <StoreContext.Provider value={store}> <App /> </StoreContext.Provider>, document.getElementById('root'), ); import {useDispatch, useMappedState} from 'redux-react-hook'; export function DeleteButton({index}) { // Declare your memoized mapState function const mapState = useCallback( state => ({ canDelete: state.todos[index].canDelete, name: state.todos[index].name, }), [index], ); // Get data from and subscribe to the store const {canDelete, name} = useMappedState(mapState); // Create actions const dispatch = useDispatch(); const deleteTodo = useCallback( () => dispatch({ type: 'delete todo', index, }), [index], ); return ( <button disabled={!canDelete} onClick={deleteTodo}> Delete {name} </button> ); }
這也是官方較爲推薦的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()這3個主要方法,分別對應與mapState、mapDispatch以及直接拿到redux中store的實例.
簡單介紹一下useSelector,在useSelector中除了能從store中拿到state之外,還支持深度比較的功能,若是相應的state先後沒有改變,就不會去從新的計算.
舉例來講,最基礎的用法:
import React from 'react' import { useSelector } from 'react-redux' export const TodoListItem = props => { const todo = useSelector(state => state.todos[props.id]) return <div>{todo.text}</div> }
實現緩存功能的用法:
import React from 'react' import { useSelector } from 'react-redux' import { createSelector } from 'reselect' const selectNumOfDoneTodos = createSelector( state => state.todos, todos => todos.filter(todo => todo.isDone).length ) export const DoneTodosCounter = () => { const NumOfDoneTodos = useSelector(selectNumOfDoneTodos) return <div>{NumOfDoneTodos}</div> } export const App = () => { return ( <> <span>Number of done todos:</span> <DoneTodosCounter /> </> ) }
在上述的緩存用法中,只要todos.filter(todo => todo.isDone).length不改變,就不會去從新計算.