可能你的react函數組件歷來沒有優化過

16.6以前,函數組件沒有像 shouldComponentUpdate這樣的方法,也沒有相似 PureComponent這種解決方案,避免不了函數組件裏面全部的代碼再次的執行,要依靠外面的條件渲染來控制,或者是高階組件。以前的話,選擇使用函數組件的狀況是一些比較簡單的又比較純的組件,只是負責展現的。並且函數組件最終編譯babel結果是隻執行 createElement那一步;class組件同樣有生命週期要實例化,最終通過Babel成es5代碼的時候還很長前端

React.memo

當16.6的memo問世,函數組件就有了相似 PureComponentshouldComponentUpdate的解決方案,memo的使用方法:react

 
 
  1. const C = (props) => {數組

  2.    return <section>{props.name}你太美</section>性能優化

  3. }babel


  4. export default React.memo(C)app

當父組件執行render的時候,避免不了C組件的渲染和C函數的執行(若是不在外面加判斷的話:{isShowC&&<C/>})。當到了C組件的時候,會淺比較C組件先後props值。若是props每個屬性值都同樣,會跳過函數組件C的執行,減小了沒必要要的渲染,達到了性能優化。ide

memo第二個參數

第二個參數,是一個函數,該函數傳入參數是新props和上次props,咱們能夠在函數裏面作判斷邏輯,控制返回值。當咱們讓函數return true的時候,告訴了react這兩個props是同樣的,不用從新執行整個函數組件;反之false的時候會從新執行該組件函數

memo(IfEqual, () => false);

好比這行代碼,判斷函數一直返回false, memo包住的 IfEqual組件不管怎樣都會從新執行性能

當咱們用上了memo,就能夠根據業務來進行優化了:優化

React.memo(C, (nextProps, prevProps) => {    // 作咱們想作的事情,相似shouldComponentUpdate})

函數組件中傳入的props值爲函數時

咱們都知道,js中函數不是簡單數據類型,也就是說 function(){}function(){}是不同的,與 {}{}不同同理。那麼咱們傳入 props.onClick(即便是長得同樣的內容徹底同樣),先後 props.onClick都不能劃上等號

    <div>      <IfEqual onClick={() => {}} />    </div>

以爲inline function很差看,那前面定義一下,實際上仍是逃不了同一個事情:它們是不同的。此次是由於,函數組件的渲染,也就是執行,每一次從新執行,函數做用域裏面一切都是從新開始。這就至關於上一次組件渲染 consthandleClick=()=>{},後面渲染又一次 consthandleClick=()=>{},它們都不是同一個東西

export default () => {  const handleClick = () => {}   return (    <div>      <IfEqual onClick={handleClick} />    </div>  )}

這種狀況下,咱們能夠用memo第二個參數來拯救多餘一次的渲染的局面:

// props: { a: 1, onClick: () => {} }// 在咱們知道onClick是作同一個事情的函數的前提下,不比較onClickReact.memo(C, (nextProps, prevProps) => nextProps.a === prevProps.a)

最後,先後props的 onClick,它們只有一種狀況是同樣的——把聲明抽到組件外面去

const handleClick = () => {} export default () => {  return (    <div>      <IfEqual onClick={handleClick} />    </div>  )}

這時候,有沒有想起class組件裏面老是 onClick={this.handleClick}呢?this.handleClick一直都是同一個函數。這種狀況,子組件爲函數組件的時候,包一層memo就能夠實現purecomponent的效果

useCallback

函數組件把函數定義寫在外面,是能夠解決問題。可是,若是handleClick依賴組件內部的一些變量,那handleClick又不得不寫在裏面(固然利用引用類型能夠解決)。或者仍是正常寫,靠memo第二個參數來控制要不要從新渲染子函數組件。可是不管怎樣,都存在一個問題,就是那一塊代碼寫在裏面呢,都沒法避免代碼的執行和函數的從新定義,好比

function a(){    const b = function(){        console.log(1)        // 不少不少代碼    }}a()a() // 函數b又被定義了一次

若是咱們經過依賴來肯定先後兩次是否是同一個函數,咱們能夠用函數記憶來實現整個功能

 
 
  1. // 半僞代碼

  2. let prev

  3. let prevDeps

  4. function memorize(fn, deps) {

  5.    // 先後依賴一致,不用從新計算直接返回上次結果

  6.    if (prev && isEqual(deps, prevDeps)) {

  7.        return prev

  8.    }

  9.    prevDeps = deps

  10.    prev = fn

  11.    return fn

  12. }


  13. function a(){

  14.    const b = memorize(function(){

  15.        console.log(1)

  16.        // 不少不少代碼

  17.    }, [])

  18. }

  19. a()

  20. a() // 函數b是同一個

相似函數記憶的原理,後來有了 useCallback的出現,多了一種新的解決方案,根據依賴生成一個函數:

const handleClick = useCallback(() => {    console.log(dep)}, [dep])

當dep不變,每一次函數組件的執行,handleClick都是同一個函數。若是dep變了,那麼handleClick又是一個新的函數

 
 
  1. export default () => {

  2. // 沒有依賴,永遠是同一個函數

  3. const handleClick = useCallback(() => {}, []);


  4. // 依賴a,從新執行函數組件,a不變的,是同一個函數

  5. // a變了handleClick是新的函數

  6. const handleClick1 = useCallback(() => {}, [a]);

  7.  return (

  8.    <div>

  9.      <IfEqual onClick={handleClick} />

  10.    </div>

  11.  )

  12. }

react組件也是一個函數,那其實 useCallback還能夠作一個函數組件:

 
 
  1. export default () => {

  2. const handleClick = useCallback(() => {}, []);

  3. const Cpn = useCallback(({ name }) => {

  4.    return <button onClick={handleClick}>{name}</button>

  5. }, [handleClick]);


  6.  return (

  7.    <div>

  8.      <Cpn name="hi" />

  9.    </div>

  10.  )

  11. }

固然這只是一個簡單的場景,若是用了hooks,尚未解決問題或者暫時沒有想到優雅的封裝技巧,想用高階組件的時候,不妨嘗試一下 useCallback

useMemo

const a = useMemo(() => memorizeValue, deps)

當deps不變,a的值仍是上次的memorizeValue,省去了從新計算的過程。若是memorizeValue是一個函數,和useCallback是同樣的效果:

useCallback(fn, inputs) <=> useMemo(() => fn, inputs)

咱們能夠試一下同步執行的代碼,當時間很是長的時候,useMemo能夠發揮它的做用了:

 
 
  1. // 強行更新組件

  2. const useForceUpdate = () => {

  3.  const forceUpdate = useState(0)[1];

  4.  return () => forceUpdate(x => x + 1);

  5. }

  6. // 一個很耗時間的代碼

  7. function slowlyAdd(n) {

  8.  console.time('add slowly')

  9.  let res = n;

  10.  for (let i = 0; i < 2000000000; i++) {

  11.    res += 1;

  12.  }

  13.  console.timeEnd('add slowly')

  14.  return res;

  15. }


  16. // useMemo記憶結果的一個自定義hook

  17. function useSlowlyAdd(n) {

  18.  const res = useMemo(() => {

  19.    return slowlyAdd(n);

  20.  }, [n])

  21.  return res;

  22. }


  23. export default () => {

  24.  const [count, add] = useState(1);

  25.  const forceUpdate = useForceUpdate();

  26.  const handleClick = useCallback(() => {}, []);

  27.  useSlowlyAdd(count) // 第一次這裏會耗不少時間,頁面卡死一陣

  28.  return (

  29.    <>

  30.      <button onClick={forceUpdate}>更新頁面</button>

  31.      <button onClick={() => add(count + 1)}>+</button>

  32.    </>

  33.  )

  34. }

第一次進來,頁面暫時沒有任何反應一陣,這是由於slowlyAdd佔用了js主線程。當咱們點擊‘更新頁面’更新的時候,頁面並無卡死,並且組件也從新渲染執行了一次。當咱們點擊+,頁面又開始卡死一陣。

這是由於點擊+的時候,修改了useMemo的依賴n,n變了從新計算,計算耗費時間。若是點擊更新頁面,沒有修改到依賴n,不會從新計算,頁面也不會卡

固然,useMemo也能夠作高階組件,用起來的時候,能夠寫成reactElement的形式了:

const HOC = useMemo(() => <C />, deps)

最後

有以下的組件,Big是一個10w個節點的組件,每個節點都綁定事件

const handleClick = useCallback(() => {}, []);export default () => {  return (    <div>      <Big onClick={handleClick} />    </div>  )}

若是Big組件沒有memo包住,首次掛載和再次渲染父組件性能以下:圖片

若是Big組件有memo包住並且props被認爲是同樣的狀況下,首次掛載和再次渲染父組件性能以下:圖片

可是性能優化不是免費午飯,不是全部的函數組件都包memo,組件裏面的函數都包usecallback就行了。由於具備memorize,沒有優化的意義的狀況下強行優化,反而是性能惡化。

總結一下對於props的某個屬性值爲函數的時候,如何作到子組件不從新執行多餘渲染:

圖片


關注咱們

IMWeb 團隊隸屬騰訊公司,是國內最專業的前端團隊之一。

咱們專一前端領域多年,負責過 QQ 資料、QQ 註冊、QQ 羣等億級業務。目前聚焦於在線教育領域,精心打磨 騰訊課堂、企鵝輔導 及 ABCMouse 三大產品。

相關文章
相關標籤/搜索