React Hooks工程實踐總結


    最近在項目中基本上所有使用了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


1、React hooks中的渲染行爲

1.React hooks組件是如何渲染的

    理解React hooks的關鍵,就是要明白,hooks組件的每一次渲染都是獨立,每一次的render都是一個獨立的做用域,擁有本身的props和states、事件處理函數等。歸納來說:git

每一次的render都是一個互不相關的函數,擁有徹底獨立的函數做用域,執行該渲染函數,返回相應的渲染結果es6

而類組件則不一樣,類組件中的props和states在整個生命週期中都是指向最新的那次渲染.github

React hooks組件和類組件的在渲染行爲中的區別,看起來很繞,咱們能夠用圖來區別,redux

未命名文件 (6)的副本

    上圖表示在React hooks組件的渲染過程,從圖中能夠看出,react hooks組件的每一次渲染都是一個獨立的函數,會生成渲染區專屬的props和state. 接着來看類組件中的渲染行爲:數組

未命名文件 (6)

    類組件中在渲染開始的時候會在類組件的構造函數中生成一個props和state,全部的渲染過程都是在一個渲染函數中進行的而且,每一次的渲染中都不會去生成新的state和props,而是將值賦值給最開始被初始化的this.props和this.state。緩存

2.工程中注意React hooks的渲染行爲

    理解了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.

3.神奇的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>
  • 渲染完成以後調用這個effect:{ document.title = 'You clicked 0 times' }。
  • 點擊Click me
  • 渲染新的內容渲染的內容: <p>You clicked 1 times</p>
  • 渲染完成以後調用這個effect:() => { document.title = 'You clicked 1 times' }。

也就是說每次渲染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建立的對象外,其餘對象和函數都沒有相關性.

2、React hooks中的性能優化

    前面咱們講了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來比較是否相同的,所以是淺比較。

3、React hooks中的狀態管理和通訊

  react hooks中的局部狀態管理相比於類組件而言更加簡介,那麼若是咱們組件採用react hooks,那麼如何解決組件間的通訊問題。

(1) UseContext

  最基礎的想法可能就是經過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的使用複雜度。

(2) Redux結合hooks來實現組件間的通訊

  hooks組件間的通訊,一樣可使用redux來實現。也就是說:

在React hooks中,redux也有其存在的意義

  在hooks中存在一個問題,由於不存在相似於react-redux中connect這個高階組件,來傳遞mapState和mapDispatch, 解決的方式是經過redux-react-hook或者react-redux的7.1 hooks版原本使用。

  • redux-react-hook

  在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 7.1的hooks版

   這也是官方較爲推薦的,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不改變,就不會去從新計算.

相關文章
相關標籤/搜索