React中immutable的UI組件渲染性能詳解

引言

react 一直遵循UI = fn(state) 的原則,有時候我們的state卻和UI不同步 有時候組件本身在業務上不需要渲染,卻又會再一次re-render。之前在項目中遇到的一些問題,這裡做一個簡單的分析,大傢可以一起交流一下

UI組件渲染性能

react每次觸發頁面的更新可大致分成兩步:

  • render(): 主要是計算v-dom的diff
  • commit階段 :將得到的diff v-dom一次性更新到真實DOM

一般我們討論的渲染 指的是第一步, 我可以悄悄的告訴你 第二步我們也管不瞭,什麼時候更新真實DOM, React有一套自己的機制

組件渲染分為首次渲染和重渲染,首次渲染不可避免就不討論 重渲染指當組件state或者props發生變化的時候造成的後續渲染過程,也是本文的討論重點

其實React 在更新組件這方面 一直都有一個詬病 就是:

父組件重渲染的時候,會遞歸重渲染所有的子組件

const List = () => {
  const [name, setName] = useState<string>("");
  // 用來測試的其它狀態值
  const [count, setCount] = useState<number>(0);
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const val = e.target.value;
    setName(val);
  };
  const handleClick = () => {
    setCount((c) => c + 1);
  };
  return (
    <main>
      <div className="list">
        <input value={name} onChange={handleInputChange} />
        <button onClick={handleClick}>測試</button>
        <Child count={count} />
      </div>
    </main>
  );
};
const Child: React.FC<any> = (props) => {
  console.log("Child has render");
  return <p>count:{props.count}</p>;
};

當 Input name改變的時候 List觸發rerender Child會發生rerender 可是Child 依賴的props隻有count而已, 如果所有的子組件都被迫渲染,計算在render花費的時間和資源有可能成為性能瓶頸.

方案一:shallow compare

React其實剛出來就提供瞭優化的手段:

  • shouldComponentUpdate: 返回false 就直接跳過組件的render過程
  • React.PureComponent: 對props進行淺比較,如果相等 則跳過render 用於class 組件
  • React.memo: 也是進行淺比較,適用於functional Component

本文設計的組件以functioal component為主 因為後面會涉及到hooks的使用,對上述例子修改:

const Child: React.FC<any> = React.memo((props) => {
  console.log("Child has render");
  return <p>count:{props.count}</p>;
}) 

很好 child沒有跟著name重渲染瞭,如果props是一個對象呢?

const List = () => {
  const [name, setName] = useState<string>("");
  // 用來測試的其它狀態值
  const [count, setCount] = useState<number>(0);
  console.log(count)
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const val = e.target.value;
    setName(val);
  };
  const handleClick = () => {
    setCount((c) => c + 1);
  };
  const item: IItem = {
    text: name,
    id: 1,
  };
  return (
    <main>
      <div className="list">
        <input value={name} onChange={handleInputChange} />
        <button onClick={handleClick}>測試</button>
        <Child  item={item} />
      </div>
    </main>
  );
};
const Child: React.FC<{ count?: number; item: IItem }> = React.memo(
  ({ item }) => {
    console.log("Child has render");
    return <p>text:{item.text}</p>;
  }
);

改變name時候Child會改變 這是預期內的 而當改變count時,Child還是會重渲染,這是什麼原因呢?因為count改變後 List組件會rerender 從而導致導致 item這個對象又重新生成瞭 導致child每次接受的是一個新的object對象 由於每個literal object的比較是引用比較 雖然前後屬性相同,但比較得出的結果為false,造成 Child rerender 。

淺比較一定要相同引用嗎?不一定,一般的面試中淺比較隻是對值的比較 但是React.memo中要求引用類型一定要相同 為什麼呢?我猜是出於對性能的考慮,不用深比較也是為瞭節約性能 通常情況下 我們想要的UI對應的是每個葉子節點的值 ,即隻要葉子節點的值不發生變化 就不要rerender

方案二:直接對前後的對象進行deepCompare

還好React.memo有第二個參數可以使用

const Child: React.FC<{ item: IItem }> = React.memo(
  ({ item }) => {
    console.log("Child has render");
    return <p>text:{item.text}</p>;
  },
  (preProps, nextProps) => {
    return _.isEqual(preProps, nextProps); // lodash的深比較 
  }
);

保證引用相等的情況下,值也相等 useRef

  const item: MutableRefObject<IItem> = React.useRef({
    text: name,
    id: 1,
  });
<Child item={item.current} />

好傢夥,name無論怎麼變化 Child 始終不會更新,useRef保證瞭返回的值是一個MutableObject 不可變的,意思就是引用完全相同 不管值變化 就不會保持更新.導致瞭UI不一致,那麼我們怎麼保證 name 不變的時候 item 和上次相等,name 改變的時候才和上次不等。useMemo

  const item: IItem = React.useMemo(
    () => ({
      text: name,
      id: 1,
    }),
    [name] // name變化觸發item不等 name不變item和上次相同
  );

總結:

  • 父組件重渲染的時候,會遞歸重渲染所有的子組件
  • 對primitive 值的數據 React比較值的相等來判斷是否重渲染組件 對Object數據 React比較引用 如果引用相同 不會重渲染,如果引用不同 會認為是不同對象 造成重渲染
  • useRef返回一個MutableRefObject數據 永遠返回的是同一個引用 直到生命周期結束,官網的註解

useRef returns a mutable ref object whose .current property is initialized to the passed argument

(initialValue). The returned object will persist for the full lifetime of the component.

useMemo 返回一個計算的值 當dep改變時 返回的值才改變(引用的改變)

以上就是React中immutable的UI組件渲染性能詳解的詳細內容,更多關於React immutable UI組件渲染的資料請關註WalkonNet其它相關文章!

推薦閱讀: