React Hooks使用避坑指南

孤舟蓑翁發表於2021-06-20

 函式元件比類元件更加方便實現業務邏輯程式碼的分離和元件的複用,函式元件也比類元件輕量,沒有react hooks之前,函式元件是無法實現LocalState的,這導致有localstate狀態的元件無法用函式元件來書寫,這限制了函式元件的應用範圍,而react hooks擴充套件了函式元件的能力。可是在使用的過程中,也要注意下面這些問題,否則就會掉進坑裡,造成效能損失。按照下面的方法做,,才能避開這些陷阱。

1. 將與狀態改變無關的變數和方法提取到元件函式外面

每次狀態改變時,整個函式元件都會重新執行一遍。導致函式元件內部定義的方法和變數,都會重新建立,重新給它們分配記憶體,這會導致效能受到影響。

import React, {useState,useCallback} from "react";

// 測試每次狀態改變時,方法是不是重新分配記憶體
let testFooMemoAlloc = new Set();

const Page = (props:any) => {
  console.log('每次狀態改變,函式元件從頭開始執行')
  const [count, setCount] = useState(0);
  const calc = () => {
    setCount(count + 1);
  }

  const bar = {
    a:1,
    b:2,
    c: '與狀態無關的變數定義'
  }
 
  const doFoo = () => {
    console.log('與狀態無關的方法');

  }
  testFooMemoAlloc.add(doFoo)
  
  return (
    <>
      <button onClick={calc}>加1</button>
      <p>count:{count}</p>
      <p>testFooMemoAlloc.size增加的話,說明每次都重新分配了記憶體:{testFooMemoAlloc.size}</p>
    </>
  )
}

export default Page;

與改變狀態相關的變數和方法,必須放在hooks元件內,而無狀態無關的變數和方法,可以提取到函式元件外,避免每次狀態更新,都重新分配記憶體。也可以分別使用useMemo和useCallback包裹變數與函式,也能達到同樣的效果,後面會講。

import React, {useState,useCallback} from "react";

// 測試每次狀態改變時,方法是不是重新分配記憶體
let testFooMemoAlloc = new Set();

const bar = {
  a:1,
  b:2,
  c: '與狀態無關的變數定義'
}

const doFoo = () => {
  console.log('與狀態無關的方法');

}

const Page = (props:any) => {
  console.log('每次狀態改變,函式元件從頭開始執行')
  const [count, setCount] = useState(0);
  const calc = () => {
    setCount(count + 1);
  }

  testFooMemoAlloc.add(doFoo)
  
  return (
    <>
      <button onClick={calc}>加1</button>
      <p>count:{count}</p>
      <p>testFooMemoAlloc.size增加的話,說明每次都重新分配了記憶體:{testFooMemoAlloc.size}</p>
    </>
  )
}

export default Page;

2. 用memo對子元件進行包裝

父元件引入子元件,會造成一些不必要的重複渲染,每次父元件更新count,子元件都會更新。

import React,{useState} from "react";
const Child = (props:any) => {
    console.log('子元件?')
    return(
        <div>我是一個子元件</div>
    );
}
const Page = (props:any) => {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={(e) => { setCount(count+1) }}>加1</button>
            <p>count:{count}</p>
            <Child />
        </>
    )
}

export default Page;

使用memo,count變化子元件沒有更新

import React,{useState,memo} from "react";
const Child = memo((props:any) => {
    console.log('子元件?')
    return(
        <div>我是一個子元件</div>
    );
})
const Page = (props:any) => {
    const [count, setCount] = useState(0);
    return (
        <>
            <button onClick={(e) => { setCount(count+1) }}>加1</button>
            <p>count:{count}</p>
            <Child />
        </>
    )
}

export default Page;

給memo傳入第二個引數,開啟物件深度比較。當子元件傳遞的屬性值未發生改變時,子元件不會做無意義的render。

memo不僅適用於函式元件,也適用於class元件,是一個高階元件,預設情況下只會對複雜物件做淺層比較,如果想做深度比較,可以傳入第二個引數。與 shouldComponentUpdate 不同的是,deepCompare 返回 true 時,不會觸發 render,如果返回 false,則會。而 shouldComponentUpdate 剛好與其相反。

import React, {useState, memo } from "react";
import deepCompare from "./deepCompare";

const Child = memo((props:any) => {
    console.log('子元件')
  return (
      <>
      <div>我是一個子元件</div>
      <div>{ props.fooObj.a}</div>
      </>
    );
}, deepCompare)

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const [fooObj, setFooObj] = useState({ a: 1, b: { c: 2 } })
  console.log('頁面開始渲染')
  const calc = () => {
    setCount(count + 1);
    if (count === 3) {
      setFooObj({ b: { c: 2 }, a: count })
    }
  }
  const doBar = () => {
    console.log('給子元件傳遞方法,測試一下是否會引起不必須的渲染')
  }
    return (
        <>
        <button onClick={calc}>加1</button>
        <p>count:{count}</p>
        <Child fooObj={fooObj} doBar={doBar} />
        </>
    )
}

export default Page;
// 深度比較兩個物件是否相等
export default function deepCompare(prevProps: any, nextProps: any) {
  const len: number = arguments.length;
  let leftChain: any[] = [];
  let rightChain: any = [];
  // // console.log({ arguments });
  //
  if (len < 2) {
    // console.log('需要傳入2個物件,才能進行兩個物件的屬性對比');
    return true;
  }
  // for (let i = 1; i < len; i++) {
  // leftChain = [];
  // rightChain = [];
  console.log({ prevProps, nextProps });
  if (!compare2Objects(prevProps, nextProps, leftChain, rightChain)) {
    // console.log('兩個物件不相等');
    return false;
  }
  // }
  // console.log('兩個物件相等');

  return true;
}

function compare2Objects(prevProps: any, nextProps: any, leftChain: any, rightChain: any) {
  var p;

  // 兩個值都為為NaN時,在js中是不相等的, 而在這裡認為相等才是合理的
  if (isNaN(prevProps) && isNaN(nextProps) && typeof prevProps === 'number' && typeof nextProps === 'number') {
    return true;
  }

  // 原始值比較
  if (prevProps === nextProps) {
    console.log('原始值', prevProps, nextProps);
    return true;
  }

  // 構造型別比較
  if (
    (typeof prevProps === 'function' && typeof nextProps === 'function') ||
    (prevProps instanceof Date && nextProps instanceof Date) ||
    (prevProps instanceof RegExp && nextProps instanceof RegExp) ||
    (prevProps instanceof String && nextProps instanceof String) ||
    (prevProps instanceof Number && nextProps instanceof Number)
  ) {
    console.log('function', prevProps.toString() === nextProps.toString());
    return prevProps.toString() === nextProps.toString();
  }

  // 兩個比較變數的值如果是null和undefined,在這裡會退出
  if (!(prevProps instanceof Object && nextProps instanceof Object)) {
    console.log(prevProps, nextProps, 'prevProps instanceof Object && nextProps instanceof Object');
    return false;
  }

  if (prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)) {
    console.log('prevProps.isPrototypeOf(nextProps) || nextProps.isPrototypeOf(prevProps)');
    return false;
  }

  // 構造器不相等則兩個物件不相等
  if (prevProps.constructor !== nextProps.constructor) {
    console.log('prevProps.constructor !== nextProps.constructor');
    return false;
  }

  // 原型不相等則兩個物件不相等
  if (prevProps.prototype !== nextProps.prototype) {
    console.log('prevProps.prototype !== nextProps.prototype');
    return false;
  }

  if (leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1) {
    console.log('leftChain.indexOf(prevProps) > -1 || rightChain.indexOf(nextProps) > -1');
    return false;
  }

  // 遍歷下次的屬性物件,優先比較不相等的情形
  for (p in nextProps) {
    if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
      console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
      return false;
    } else if (typeof nextProps[p] !== typeof prevProps[p]) {
      console.log('typeof nextProps[p] !== typeof prevProps[p]');
      return false;
    }
  }
  // console.log('p in prevProps');
  // 遍歷上次的屬性物件,優先比較不相等的情形
  for (p in prevProps) {
    // 是否都存在某個屬性值
    if (nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)) {
      console.log('nextProps.hasOwnProperty(p) !== prevProps.hasOwnProperty(p)');
      return false;
    }
    // 屬性值的型別是否相等
    else if (typeof nextProps[p] !== typeof prevProps[p]) {
      console.log('typeof nextProps[p] !== typeof prevProps[p]');
      return false;
    }

    console.log('typeof prevProps[p]', typeof prevProps[p]);
    switch (typeof prevProps[p]) {
      // 物件型別和函式型別的處理
      case 'object':
      case 'function':
        leftChain.push(prevProps);
        rightChain.push(nextProps);

        if (!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)) {
          console.log('!compare2Objects(prevProps[p], nextProps[p], leftChain, rightChain)');
          return false;
        }

        leftChain.pop();
        rightChain.pop();
        break;

      default:
        // 基礎型別的處理
        if (prevProps[p] !== nextProps[p]) {
          return false;
        }
        break;
    }
  }

  return true;
}

3.用useCallback對元件方法進行包裝

當父元件傳遞方法給子元件的時候,memo好像沒什麼效果,無論是用const定義的方法,還在用箭頭函式或者bind定義的方法,子元件還是執行了

import React, { useState,memo } from 'react';
//子元件會有不必要渲染的例子
interface ChildProps {
  changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('普通函式子元件')
  return(
      <>
          <div>我是普通函式子元件</div>
          <button onClick={changeName}>普通函式子元件按鈕</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('箭頭函式子元件')
  return(
      <>
          <div>我是箭頭函式子元件</div>
          <button onClick={changeName.bind(null,'test')}>箭頭函式子元件按鈕</button>
      </>
  );
}
const ArrowMemo = memo(ArrowChild);

const BindChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('Bind函式子元件')
  return(
      <>
          <div>我是Bind函式子元件</div>
          <button onClick={changeName}>Bind函式子元件按鈕</button>
      </>
  );
}
const BindMemo = memo(BindChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const name = "test";

  const changeName = function() {
    console.log('測試給子元件傳遞方法,使用useCallback後,子元件是否還會進行無效渲染');
  }

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <ArrowMemo  changeName={()=>changeName()}/>
          <BindMemo  changeName={changeName.bind(null)}/>
          <FunMemo changeName={changeName} />
      </>
  )
}

export default Page;

 

使用useCallback,引數為[],頁面初始渲染後,改變count的值,傳遞普通函式的子元件不再渲染, 傳遞箭頭函式和bind方式書寫的方法的子元件還是會渲染

import React, { useState,memo ,useCallback} from 'react';
//子元件會有不必要渲染的例子
interface ChildProps {
  changeName: ()=>void;
}
const FunChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('普通函式子元件')
  return(
      <>
          <div>我是普通函式子元件</div>
          <button onClick={changeName}>普通函式子元件按鈕</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const ArrowChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('箭頭函式子元件')
  return(
      <>
          <div>我是箭頭函式子元件</div>
          <button onClick={changeName.bind(null,'test')}>箭頭函式子元件按鈕</button>
      </>
  );
}
const ArrowMemo = memo(ArrowChild);

const BindChild = ({ changeName}: ChildProps): JSX.Element => {
  console.log('Bind函式子元件')
  return(
      <>
          <div>我是Bind函式子元件</div>
          <button onClick={changeName}>Bind函式子元件按鈕</button>
      </>
  );
}
const BindMemo = memo(BindChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const name = "test";

  const changeName = useCallback(() => {
    console.log('測試給子元件傳遞方法,使用useCallback後,子元件是否還會進行無效渲染');
  },[])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <ArrowMemo  changeName={()=>changeName()}/>
          <BindMemo  changeName={changeName.bind(null)}/>
          <FunMemo changeName={changeName} />
      </>
  )
}

export default Page;

 4.用useMemo對元件中的物件變數進行包裝

在子元件使用了memo,useCallback的情況下,給子元件傳遞一個物件屬性,物件值和方法都未發生改變的情況下,父元件無關狀態變更,子元件也會重新渲染。

import React, { useState,memo ,useCallback} from 'react';
//子元件會有不必要渲染的例子-使用了memo,useCallback的情況下,給子元件傳遞一個物件屬性值
interface ChildProps {
  childStyle: { color: string; fontSize: string;};
  changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
  console.log('普通函式子元件')
  return(
      <>
          <div style={childStyle}>我是普通函式子元件</div>
          <button onClick={changeName}>普通函式子元件按鈕</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const childStyle = {color:'green',fontSize:'16px'};

  const changeName = useCallback(() => {
    console.log('測試給子元件傳遞方法,使用useCallback後,子元件是否還會進行無效渲染');
  },[])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <FunMemo childStyle={childStyle} changeName={changeName} />
      </>
  )
}

export default Page;

 

 

 使用useMemo可以解決給子元件傳遞物件屬性時的不必要更新問題。

import React, { useState,memo, useMemo, useCallback} from 'react';
//子元件會有不必要渲染的例子
interface ChildProps {
  childStyle: { color: string; fontSize: string;};
  changeName: ()=>void;
}
const FunChild = ({ childStyle,changeName}: ChildProps): JSX.Element => {
  console.log('普通函式子元件')
  return(
      <>
          <div style={childStyle}>我是普通函式子元件</div>
          <button onClick={changeName}>普通函式子元件按鈕</button>
      </>
  );
}
const FunMemo = memo(FunChild);

const Page = (props:any) => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");
  const childStyle = {color:'green',fontSize:'16px'};

  const changeName = useCallback(() => {
    setName('變一下名稱')
  }, [])
  const childStyleMemo = useMemo(() => {
    return {
      color: name === '變一下名稱' ? 'red':'green',
      fontSize: '16px'
    }
  }, [name])

  return (
      <>
          <button onClick={(e) => { setCount(count+1) }}>加1</button>
          <p>count:{count}</p>
          <FunMemo childStyle={childStyleMemo} changeName={changeName} />
      </>
  )
}

export default Page;

 

相關文章