react Hooks 實踐總結

最近在項目中實踐了一下 react hooks,結合本身的經驗,整理了一些文檔,這裏記錄一下java

經常使用的官方 hooks(官方文檔

useState

這個應該是咱們最經常使用的 hook 了,用來代替 class 組件中的 setState
基礎用法:const [state, setState] = useState(initialState);
react

須要注意的點【查看代碼
redux

  • setState 這裏接受的參數除了一個給定的值外,還能夠接受一個函數。例如
const [count, setCont] = useState(0);

setCount(count+1)
setCount((prevCount) => prevCount + 1)
// 上面用法一般使用在,我無法保證我獲取的 count 是最新的值的狀況下,好比在 useCallback 中

const fn = useCallback(() => {
  console.log(count); // 這裏的count 會一直是初始化的那個count,這個時候若是用 setCount(count + 1) 就會出現問題
  setCount((prevCount) => prevCount + 1); // 這樣就不會有問題
}, []) // 這裏最好傳遞要用到的依賴,不然會被緩存,獲取不到最新的值

複製代碼
  • setState 會對值進行比較,若是 prevValue 跟此次的 value 相等的話(比較只是進行了淺比較),將不會觸發更新。若是想強制每次更新,能夠自定義一個hooks,例如
// 若是是class 組件,每次調用setState 都會觸發render,它不會對先後的值進行比較
const useFocusUpdate = () => {
  const [ignore, update] = useState(0);
  return () => {
    update(ignore + 1);
  };
};
複製代碼
  • initialState 參數只會在組件的初始渲染中起做用,後續渲染時會被忽略。

useEffect

基礎用法: useEffect(fn, [deps]) 【後面的Fn, deps,均指的此處,Fn2指的是 Fn的返回函數】
默認狀況下useEffect 接受的函數 Fn 會在 render 以後執行,Fn 執行後,若是返回了一個函數Fn2,那麼Fn2,在執行下一個 effect 以前,即上一個 effect 銷燬時數組

export default function App() {
  console.log('start')
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('log on effect', count);
    return () => {
      console.log('log on effecct return fn', count);
    }
  }, [count])
  console.log('render')
  return (
    <div className="App"> <h1>{count}</h1> <h2 onClick={() => setCount(count + 1)}>Add Count</h2> </div>
  );
}

// 打印順序
// start render log on effect 0
// 點擊 Add Count 觸發 count 更新,打印順序
// start render log on effecct return fn 0 log on effect 1
複製代碼

須要注意的點緩存

  • 在Fn中使用到的變量須要傳入的 deps,不然會出現取不到最新的值的問題
  • 傳入 deps 的值也須要注意,不要傳遞函數中的方法,例如
// 這樣會形成死循環,由於每一次從新 render handle 就會被從新建立,useEffect 每次都會檢測到變化,每次就會執行
function App() {
  const [count, setCount] = useState(0);
  const handle = () => { setCount(count + 1) };
  useEffect(() => {
    handle();
  }, [fn])
  return (
    <div className="App"> </div>
  );
}
複製代碼

useEffect 常常被用到的使用場景微信

  • 模擬 DidMount
    useEffect(fn, []) 傳入一個空數組便可,fn 會在首次 render 後執行,以後不再會執行,fn 返回的函數會在組件銷燬時執行,即爲 willUnmountapp

  • 用來監變量變化,例如dom

// 當 city 出現變化後,咱們從新開始拉取 城市信息,城市下面的運營信息,城市下面的POI等信息
useEffect(() => {
  if (cityId !== null) {
    fetchCityInfo();
    fetchCategoryList();
    fetchOperationResource();
    fetchPoiList();
  }
}, [cityId]);
複製代碼

useCallback

把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。
ide

爲何會有這個hook呢?由於前面說的,組件更新的時候,組件內的內聯函數都會被從新建立,若是咱們將這個內聯函數變成依賴,或者傳遞給某個子組件,都會致使一些性能問題。模塊化

應用場景

// 這種寫法,若是 App 須要re-render,Children 也會出現 re-render,由於handle這個函數每次都被從新被建立了
function App() {
  const handle = () => { ... }
  return (
    <div className="App">
      <Children onClick={handle}/>
    </div>
  );
}
  
function App() {
  const handle = useCallback(fn, [deps]); // fn 中若是用到了某個state,須要放在deps中,不然拿到的 state 不是最新的
  return (
    <div className="App">
      <Children onClick={handle}/>
    </div>
  );
}
複製代碼

useMemo

把「建立」函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。

useCallback(fn, deps) 至關於 useMemo(() => fn, deps)

應用場景

  • 須要複雜計算的時候,能夠用來存儲, 例如
function App() {
  // computeExpensiveValue 是一個很是複雜的計算函數,這樣作了後,只要依賴的 a 不發生變化,
  // computeExpensiveValue是不會重複執行的,value是一個 memoized 值。
  const value = useMemo(() => computeExpensiveValue(a), [a]);
  return (
    <div className="App"> {value} </div>
  );
}
複製代碼
  • 用 useMemo 來優化引用數據的傳遞,例如
// 這種寫法,若是 App 須要re-render,Children 也會出現 re-render,由於 Children 的 style 這個值一直都被從新建立
function App({ height }) {
  return (
    <div className="App">
      <Children style={{ height }}/>
    </div>
  );
}

// 用 useMemo 來記憶,減小 Children 的 re-render
function App({ height }) {
  const childrenStyle = useMemo(() => ({ height }), [height])
  return (
    <div className="App">
      <Children style={childrenStyle}/>
    </div>
  );
}
複製代碼

useRef

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。

基本用法跟注意點【查看代碼

  • 用來訪問dom節點,或者訪問一堆列表的節點
// 注意 須要首次渲染結束後,ref.current 上面纔有對應的 dom。若是 dom 被移除了對應的 ref.currnt 就會變成 null
export default function App() {
  const [show, setVisible] = useState(true);
  const ref = useRef(null);
  const listRef = useRef([]);
  useEffect(() => {
    console.log(listRef);
  }, [ref2])
  
  return (
    <div className="App"> {show && <div ref={ref}>dom1</div>} {ary.map((num, index) => <div key={num} ref={(_ref) => { listRef.current[index] = _ref; }}>{num}</div>)} <div onClick={() => { console.log(ref.current); setVisible(!show) }}>click me</div> </div>
  );
}
複製代碼
  • ref是不能被監聽的
export default function App() {
  const ref2 = useRef(0);
  useEffect(() => {
    // ref2 改變後 並不會從新觸發此函數
    console.log(ref2);
  }, [ref2])
  return (
    <div className="App"> <div onClick={() => { ref2.current = Math.random(); }}> change ref</div> </div>
  );
}
複製代碼
  • 用來存儲一些不用引發 re-render 可是須要變化的變量。例如
export default function App() {
  const hadSendMc = useRef(false);

  return (
    <div className="App"> <div onClick={() => { if(hadSendMc.current){ return; } console.log('send mc') hadSendMc.current = true; }}>send mc</div> </div>
複製代碼

useImperativeHandle

useImperativeHandle 可讓你在使用 ref 時自定義暴露給父組件的實例值。在大多數狀況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應當與 forwardRef 一塊兒使用。
我在項目中只用到了一次,主要是給父組件暴露自定義的方法,而不是一股腦的直接返回ref,房子父組件操做ref致使問題

使用示例

const Video = ({ ... }: PropTypes, ref: any) => {
  const videoRef: any = useRef(null);
  const playVideo = () => {...};
  const handlePlayBtnClick = () => {...};

  useImperativeHandle(ref, () => ({
    play: playVideo,
    pause: () => {
      if (videoRef.current) {
        videoRef.current.pause();
        setVideoVisible(false);
      }
    },
  }));  

  return (
    <div className={styles.container}> <video ref={videoRef}> <source src={videoUrl} type="video/mp4" /> </video> </div> ); }; export default React.memo(React.forwardRef(Video)); 複製代碼

後續若是使用了,我會同步補上
useContext (這個我沒有用過,略)
useReducer (這個我沒有用過,略)
useLayoutEffect (這個我沒有用過,略)
useDebugValue (這個我沒有用過,略)

經常使用的優化手段

  • 使用 React.memo 去優化
    因爲 Function Component 組件沒有相似 shouldComponentUpdate 這樣的方法,也無法繼承 PureComponent,咱們就可使用 React.memo去作優化
    注意:
    React.memo僅影響 props 變動。若是函數組件被 React.memo 包裹,且其實現中擁有 useState 或 useContext 的 Hook,當 context 發生變化時,它仍會從新渲染。
    使用示例:React.memo(MyComponent, areEqual);
    默認狀況下其只會對複雜對象作淺層對比,若是你想要控制對比過程,那麼請將自定義的比較函數經過第二個參數傳入來實現。

  • 利用 useCallback 跟 useMemo 去緩存數據,具體能夠看上面的示例

  • 若是使用了 react-redux,能夠經過 useSelect 的第二個參數去作優化
    使用示例:const result : any = useSelector(selector : Function, areEqual? : Function)

項目中自定義的 hook 梳理

針對具體的業務具體開發,筆者這裏只是拋磚迎玉

  • 發送頁面PV
export const usePageView = (pageId) => {
  const hadSendPv = useRef(false);

  useEffect(() => {
    if (hadSendPv.current) {
      return;
    }
    sendPv();
  }, [pageId]);
};
複製代碼
  • useIntersectionObserver 用來監聽 dom 是否出如今可視窗口。注意須要引用 intersection-observer-polyfill
export const useIntersectionObserver = (cb: ObserveCallback, ref?: any, startObserver = true) => {
  const io: any = useMemo(() => new IntersectionObserver((IntersectionObserverEntryList: any) => cb(IntersectionObserverEntryList, io)), [cb]);
  const observerRef: any = useRef(ref || null);

  useEffect(() => {
    if (!observerRef.current || !startObserver) {
      return;
    }
    if (observerRef.current instanceof Array) {
      const eleList = observerRef.current;
      eleList.forEach((ele: any) => {
        io.observe(ele);
      });
      return () => {
        eleList.forEach((ele: any) => {
          io.unobserve(ele);
        });
      };
    }
    const ele = observerRef.current;
    io.observe(ele);

    return () => {
      io.unobserve(ele);
    };
  }, [io, startObserver]);
  return observerRef;
};

// 使用方法
function App({ height }) {
  const observerHandle = useCallback(() => { ... }, []);
  const observerRef = useIntersectionObserver(observerHandle)
  return (
    <div className="App" ref={observerRef}> <Children style={{ height }}/> </div> ); } 複製代碼
  • 抽離微信分享設置
const environment = { ... };
export const useWechatShare = (shareInfo: any) => {
  if ((environment.isMini() || environment.isWx) && shareInfo.shareUrl) {
    // 微信分享設置
  }
  }, [shareInfo, environment]);
};
複製代碼
  • useScroll 頁面滾動函數綁定 注意:此處不是相對於body滾動的!
export const useScroll = (handle: ScrollHandle, threshHold: number = 250) => {
  const handleWrapper = useCallback(throttle(handle, threshHold), [handle]);
  const containerId = 'containerId';

  useEffect(() => {
    let listerEle: any = window;
    if (containerId) {
      listerEle = document.getElementById(containerId);
    }
    console.log('useScroll 注入成功');
    if (listerEle) {
      listerEle.addEventListener('scroll', handleWrapper);
      return () => {
        listerEle.removeEventListener('scroll', handleWrapper);
      };
    }
  }, [handleWrapper, containerId]);
 }
複製代碼

總結

  1. react hooks 的出現提供了咱們更好的模塊化代碼的方式,咱們從之前的抽離公共UI的方式,能夠變爲更小粒度的拆分,例如拆分某個公共的邏輯
  2. react hooks 可以大大的減小咱們大代碼量,不須要再去理解複雜的class Component,一切都變的更加簡單了
  3. 在使用的過程當中必定要注意到,Function Component 始終是函數,只要re-render,裏面的代碼都會從新執行,內聯函數,內聯對象都會被從新建立

最後再立個 flag

用 react hooks 模擬 class 的各個生命週期

相關文章
相關標籤/搜索