全面擁抱React-Hooks

醜話說在前面
強烈建議至少刷一遍 《官方文檔》,反覆研讀 《Hooks FAQ》
這裏主要以本人關注點聚合,方便理解用於實踐

1、React-Hooks要解決什麼?

如下是上一代標準寫法類組件的缺點,也正是hook要解決的問題css

  • 大型組件很難拆分和重構,也很難測試。
  • 業務邏輯分散在組件的各個方法之中,致使重複邏輯或關聯邏輯。
  • 組件類引入了複雜的編程模式,好比 Render props 和高階組件

設計目的html

  • 增強版函數組件,徹底不使用"類",就能寫出一個全功能的組件
  • 組件儘可能寫成純函數,若是須要外部功能和反作用,就用鉤子把外部代碼"鉤"進來

2、如何用好React-Hooks?

明確幾點概念

  • 全部的hook,在默認沒有依賴項數組每次渲染都會更新
  • 每次 Render 時Props、State、事件處理、Effect等hooks都遵循 Capture Value 的特性
  • Render時會註冊各類變量,函數包括hooks,N次Render就會有N個互相隔離的狀態做用域
  • 若是你的useEffect依賴數組爲[],那麼它初始化一次,且使用的state,props等永遠是他初始化時那一次Render保存下來的值
  • React 會確保 setState,dispatch,context 函數的標識是穩定的,能夠安全地從 hooks 的依賴列表中省略

Function Component中每次Render都會造成一個快照並保留下來,這樣就確保了狀態可控,hook默認每次都更新,會致使重複請求等一系列問題,若是給[]就會一塵不變,所以用好hooks最重要就是學會控制它的變化react

3、一句話歸納Hook API

  • useState 異步設置更新state
  • useEffect 處理反作用(請求,事件監聽,操做DOM等)
  • useContext 接收一個 context 對象並返回該 context 的當前值
  • useReducer 同步處理複雜state,減小了對深層傳遞迴調的依賴
  • useCallback 返回一個 memoized 回調函數,避免非必要渲染
  • useMemo 返回一個 memoized 值,使得控制具體子節點什麼時候更新變得更容易,減小了對純組件的須要,可替代shouldComponentUpdate
  • useRef 返回一個在組件的整個生命週期內保持不變 ref 對象,其 .current 屬性是可變的,能夠繞過 Capture Value 特性
  • useLayoutEffect 其函數簽名與 useEffect 相同,但它會在全部的 DOM 變動以後同步調用 effect
  • useImperativeHandle 應當與 forwardRef 一塊兒使用,將 ref 自定義暴露給父組件的實例值
  • useDebugValue 可用於在 React 開發者工具中顯示自定義 hook 的標籤

4、關注異同點

useState 與 this.setState

  • 相同點:都是異步的,例如在 onClick 事件中,調用兩次 setState,數據只改變一次。
  • 不一樣點:類中的 setState 是合併,而useState中的 setState 是替換。

useState 與 useReducer

  • 相同點:都是操做state
  • 不一樣點:使用 useState 獲取的 setState 方法更新數據時是異步的;而使用 useReducer 獲取的 dispatch 方法更新數據是同步的。
  • 推薦:當 state 狀態值結構比較複雜時,使用useReducer

useLayoutEffect 與 useEffect

  • 相同點:都是在瀏覽器完成佈局與繪製以後執行反作用操做
  • 不一樣點:useEffect 會延遲調用,useLayoutEffect 會同步調用阻塞視覺更新,可使用它來讀取 DOM 佈局並同步觸發重渲染
  • 推薦:一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect

useCallback 與 useMemo

  • 相同點:都是返回memoized,useCallback( fn, deps) 至關於 useMemo( ( ) => fn, deps)
  • 不一樣點:useMemo返回緩存的變量,useCallback返回緩存的函數
  • 推薦:不要過早的性能優化,搭配食用口味更佳(詳見下文性能優化)

5、性能優化

在大部分狀況下咱們只要遵循 React 的默認行爲,由於 React 只更新改變了的 DOM 節點,不太重新渲染仍然花費了一些時間,除非它已經慢到讓人注意了git

react中性能的優化點在於:

  • 一、調用setState,就會觸發組件的從新渲染,不管先後的state是否不一樣
  • 二、父組件更新,子組件也會自動的更新

以前的解決方案

基於上面的兩點,咱們一般的解決方案是:github

  • 使用immutable進行比較,在不相等的時候調用setState;
  • 在 shouldComponentUpdate 中判斷先後的 props和 state,若是沒有變化,則返回false來阻止更新。
  • 使用 React.PureComponent

使用hooks function以後的解決方案

傳統上認爲,在 React 中使用內聯函數對性能的影響,與每次渲染都傳遞新的回調會如何破壞子組件的 shouldComponentUpdate 優化有關, 使用useCallback緩存函數引用,再傳遞給通過優化的並使用引用相等性去避免非必要渲染的子組件時,它將很是有用編程

  • 一、使用 React.memo等效於 PureComponent,但它只比較 props,且返回值相反,true纔會跳過更新
const Button = React.memo((props) => {
  // 你的組件
}, fn);// 也能夠自定義比較函數
  • 二、用 useMemo 優化每個具體的子節點(詳見實踐3)
  • 三、useCallback Hook 容許你在從新渲染之間保持對相同的回調引用以使得 shouldComponentUpdate 繼續工做(詳見實踐3)
  • 四、useReducer Hook 減小了對深層傳遞迴調的依賴(詳見實踐2)

如何惰性建立昂貴的對象?

  • 當建立初始 state 很昂貴時,咱們能夠傳一個 函數 給 useState 避免從新建立被忽略的初始 state
function Table(props) {
  // ⚠️ createRows() 每次渲染都會被調用
  const [rows, setRows] = useState(createRows(props.count));
  // ...
  // ✅ createRows() 只會被調用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  • 避免從新建立 useRef() 的初始值,確保某些命令式的 class 實例只被建立一次:
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都會被建立
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
function Image(props) {
  const ref = useRef(null);
  // ✅ IntersectionObserver 只會被惰性建立一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 當你須要時,調用 getObserver()
  // ...
}

6、注意事項

Hook 規則

  • 在最頂層使用 Hook
  • 只在 React 函數中調用 Hook,不要在普通的 JavaScript 函數中調用
  • 將條件判斷放置在 hook 內部
  • 全部 Hooks 必須使用 use 開頭,這是一種約定,便於使用 ESLint 插件 來強制 Hook 規範 以免 Bug;
useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

告訴 React 用到了哪些外部變量,如何對比依賴

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // 以useEffect爲示例,適用於全部hook

直到 name 改變時的 Rerender,useEffect 纔會再次執行,保證了性能且狀態可控segmentfault

不要在hook內部set依賴變量,不然你的代碼就像旋轉的菊花同樣停不下來

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);// 以useEffect爲示例,適用於全部hook

不要在useMemo內部執行與渲染無關的操做

  • useMemo返回一個 memoized 值,把「建立」函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值,避免在每次渲染時都進行高開銷的計算。
  • 傳入 useMemo 的函數會在渲染期間執行,請不要在這個函數內部執行與渲染無關的操做。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

7、實踐場景示例

實際應用場景每每不是一個hook能搞定的,長篇大論未必說的清楚,直接上例子(來源於官網摘抄,網絡收集,自我總結)數組

一、只想執行一次的 Effect 裏須要依賴外部變量

【將更新與動做解耦】-【useEffect,useReducer,useState】瀏覽器

  • 1-一、使用setState的函數式更新解決依賴一個變量

該函數將接收先前的 state,並返回一個更新後的值緩存

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
  • 1-二、使用useReducer解決依賴多個變量
import React, { useReducer, useEffect } from "react";

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  console.log(count);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

二、大型的組件樹中深層傳遞迴調

【經過 context 往下傳一個 dispatch 函數】-【createContext,useReducer,useContext】

/**index.js**/
import React, { useReducer } from "react";
import Count from './Count'
export const StoreDispatch = React.createContext(null);
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      throw new Error();
  }
}
export default function Counter() {
  // 提示:`dispatch` 不會在從新渲染之間變化
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreDispatch.Provider value={dispatch}>
      <Count state={state} />
    </StoreDispatch.Provider>
  );
}

/**Count.js**/
import React, { useEffect,useContext }  from 'react';
import {StoreDispatch} from '../index'
import styles from './index.css';

export default function(props) {
  const { count, step } = props.state;
  const dispatch = useContext(StoreDispatch);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <div className={styles.normal}>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </div>
  );
}

三、代碼內聚,更新可控

【層層依賴,各自管理】-【useEffect,useCallback,useContext】

function App() {
  const [count, setCount] = useState(1);
  const countRef = useRef();// 在組件生命週期內保持惟一實例,可穿透閉包傳值

  useEffect(() => {
    countRef.current = count; // 將 count 寫入到 ref
  });
  // 只有countRef變化時,纔會從新建立函數
  const callback = useCallback(() => {
    const currentCount = countRef.current //保持最新的值
    console.log(currentCount);
  }, [countRef]);
  return (
    <Parent callback={callback} count={count}/>
  )
}
function Parent({ count, callback }) {
  // count變化纔會從新渲染
  const child1 = useMemo(() => <Child1 count={count} />, [count]);
  // callback變化纔會從新渲染,count變化不會 Rerender
  const child2 = useMemo(() => <Child2 callback={callback} />, [callback]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

8、自定義 HOOK

獲取上一輪的 props 或 state

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

只在更新時運行 effect

function useUpdate(fn) {
  const mounting = useRef(true);
  useEffect(() => {
    if (mounting.current) {
      mounting.current = false;
    } else {
      fn();
    }
  });
}

組件是否銷燬

function useIsMounted(fn) {
  const [isMount, setIsMount] = useState(false);
  useEffect(() => {
    if (!isMount) {
      setIsMount(true);
    }
    return () => setIsMount(false);
  }, []);
  return isMount;
}

惰性初始化useRef

function useInertRef(obj) { // 傳入一個實例 new IntersectionObserver(onIntersect)
  const ref = useRef(null);
  if (ref.current === null) {
  // ✅ IntersectionObserver 只會被惰性建立一次
    ref.current = obj;
  }
  return ref.current;
}

參考文章

  1. 精讀《useEffect 徹底指南》
  2. 精讀《Function VS Class 組件》
  3. 精讀《怎麼用 React Hooks 造輪子》
  4. 《React Hooks 使用詳解》
  5. 《useMemo與useCallback使用指南》
相關文章
相關標籤/搜索