<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入 React 函數(shù)組件的 re-render 原理及優(yōu)化

          共 7603字,需瀏覽 16分鐘

           ·

          2021-12-16 03:01

          對(duì)于函數(shù)組件的 re-render,大致分為以下三種情況:

          • 組件本身使用?useState?或?useReducer?更新,引起的 re-render;

          • 父組件更新引起的 re-render;

          • 組件本身使用了?useContextcontext?更新引起的 re-render。


          下面我們將詳細(xì)討論這些情況。

          PS:如無特殊說明,下面的組件都指函數(shù)組件。

          1、組件本身使用 useState 或 useReducer 更新,引起的 re-render

          1.1、常規(guī)使用

          以計(jì)數(shù)組件為例,如下每次點(diǎn)擊 add,都會(huì)打印 'counter render',表示引起了 re-render :

          const Counter = () => {
          console.log('counter render');
          const [count, addCount ] = useState(0);
          return (
          <div className="counter">
          <div className="counter-num">{count}div>

          <button onClick={() => {addCount(count + 1)}}>addbutton>
          div>
          )
          }

          1.2、immutation state

          下面我們將上面計(jì)數(shù)組件中的 state 值改成引用類型試試,如下,發(fā)現(xiàn)點(diǎn)擊并不會(huì)引起 re-render:

          const Counter = () => {
          console.log("counter render");
          const [count, addCount] = useState({ num: 0, time: Date.now() });
          const clickHandler = () => {
          count.num++;
          count.time = Date.now();
          addCount(count);
          };
          return (
          <div className="counter">
          <div className="counter-num">
          {count.num}, {count.time}
          div>

          <button onClick={clickHandler}>addbutton>
          div>
          );
          };

          真實(shí)的原因在于,更新 state 的時(shí)候,會(huì)有一個(gè)新老 state 的比較,用的是?Object.is?進(jìn)行比較,如果為 true 則直接返回不更新,源碼如下(objectIs 會(huì)先判斷?Object.is?是否支持,如果不支持則重新實(shí)現(xiàn),eagerState 就是 oldState ):

          if (objectIs(eagerState, currentState)) {
          return;
          }

          所以更新 state 時(shí)候要注意,state 為不可變數(shù)據(jù),每次更新都需要一個(gè)新值才會(huì)有效。

          1.3、強(qiáng)制更新

          相比于類組件有個(gè)?forceUpdate?方法,函數(shù)組件是沒有該方法的,但是其實(shí)也可以自己寫一個(gè),如下,由于?Object.is({}, {})?總是?false,所以總能引起更新:

          const [, forceUpdate] = useState({});
          forceUpdate({})

          說完?useState?的更新,其實(shí)?useReducer?就不用說了,因?yàn)樵创a里面?useState?的更新其實(shí)調(diào)用的就是?useReducer?的更新,如下:

          function updateState(initialState) {
          return updateReducer(basicStateReducer);
          }

          2、父組件更新引起子組件的 re-render

          2.1、常規(guī)使用

          現(xiàn)在稍微改造上面計(jì)數(shù)的組件,添加一個(gè)子組件?Hello,如下點(diǎn)擊會(huì)發(fā)現(xiàn),每次都會(huì)輸出 "hello render",也就是說,每次更新都引起了?Hello?的 re-render,但是其實(shí)?Hello?組件的屬性根本就沒有改變:

          const Hello = ({ name }) => {
          console.log("hello render");
          return<div>hello {name}div>;
          };

          const App = () => {
          console.log("app render");
          const [count, addCount] = useState(0);
          return (
          <div className="app">
          <Hello name="react" />
          <div className="counter-num">{count}div>

          <button
          onClick={() =>
          {
          addCount(count + 1);
          }}
          >
          add
          button>
          div>
          );
          };

          對(duì)于這種不必要的 re-render,我們有手段可以優(yōu)化,下面具體聊聊。

          2.2、優(yōu)化組件設(shè)計(jì)

          2.2.1、將更新部分抽離成單獨(dú)組件

          如上,我們可以講計(jì)數(shù)部分單獨(dú)抽離成?Counter?組件,這樣計(jì)數(shù)組件的更新就影響不到?Hello?組件了,如下:

          const App = () => {
          console.log("app render");
          return (
          <div className="app">
          <Hello name="react" />
          <Counter />
          div>

          );
          };

          2.2.2、將不需要 re-render 的部分抽離,以插槽形式渲染(children)

          // App 組件預(yù)留 children 位
          const App = ({ children }) => {
          console.log("app render");
          const [count, addCount] = useState(0);
          return (
          <div className="app">
          {children}
          <div className="counter-num">{count}div>

          <button
          onClick={() =>
          {
          addCount(count + 1);
          }}
          >
          add
          button>
          div>
          );
          };

          // 使用
          <App>
          <Hello name="react" />
          App>

          除此以外,也可以以其他屬性的方式傳入組件,其本質(zhì)就是傳入的變量,所以也不會(huì)引起 re-render 。

          2.3、React.memo

          對(duì)于是否需要 re-render,類組件提供了兩種方法:PureComponent?組件和?shouldComponentUpdate?生命周期方法。

          對(duì)于函數(shù)組件來說,有一個(gè)?React.memo?方法,可以用來決定是否需要 re-render,如下我們將?Hello?組件 memo 化,這樣點(diǎn)擊更新數(shù)字的時(shí)候,?Hello?組件是不會(huì) re-render 的。除非?Hello?組件的 props 更新:

          const Hello = React.memo(({ name }) => {
          console.log("hello render");
          return<div>hello {name}div>;
          });

          const App = () => {
          console.log("app render");
          const [count, addCount] = useState(0);
          return (
          <div className="app">
          <Hello name="react" />
          <div className="counter-num">{count}div>

          <button
          onClick={() =>
          {
          addCount(count + 1);
          }}
          >
          add
          button>
          div>
          );
          };

          memo 方法的源碼定義簡(jiǎn)略如下:

          exportfunction memo<Props>(
          type: React$ElementType, // react 自定義組件
          compare?: (oldProps: Props, newProps: Props) => boolean, // 可選的比對(duì)函數(shù),決定是否 re-render
          )
          {
          ...
          const elementType = {
          $$typeof: REACT_MEMO_TYPE,
          type,
          compare: compare === undefined ? null : compare,
          };
          ...

          return elementType;
          }

          memo 的關(guān)鍵比對(duì)邏輯如下,如果有傳入 compare 函數(shù)則使用 compare 函數(shù)決定是否需要 re-render,否則使用淺比較?shallowEqual?決定是否需要 re-render:

          var compare = Component.compare;
          compare = compare !== null ? compare : shallowEqual;

          if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
          }

          既然默認(rèn)不傳 compare 時(shí),用的是淺對(duì)比,那么對(duì)于引用類的 props,就要注意了,尤其是事件處理的函數(shù),如下,我們給?Hello?組件添加一個(gè)點(diǎn)擊事件,這時(shí)我們發(fā)現(xiàn)每次點(diǎn)擊計(jì)數(shù),Hello?組件又開始 re-render 了:

          // 新增 onClick 處理函數(shù)
          const Hello = memo(({ name, onClick }) => {
          console.log("hello render");
          return<div onClick={onClick}>hello {name}div>;
          });

          const App = ({ children }) => {
          console.log("counter render");
          const [count, addCount] = useState(0);

          // 新增處理函數(shù)
          const clickHandler = () => {
          console.log("hello click");
          };

          return (
          <div className="counter">
          <Hello name="react" onClick={clickHandler} />
          <div className="counter-num">{count}div>

          <button
          onClick={() =>
          {
          addCount(count + 1);
          }}
          >
          add
          button>
          div>
          );
          };

          這是因?yàn)槊看吸c(diǎn)擊計(jì)數(shù),都會(huì)重新定義?clickHandler?處理函數(shù),這樣?shallowEqual?淺比較發(fā)現(xiàn)?onClick?屬性值不同,于是將會(huì)進(jìn)行 re-render。

          2.3.1、useCallback

          這個(gè)時(shí)候我們可以使用 useCallback 將定義的函數(shù)緩存起來,如下就不會(huì)引起 re-render 了

          // 新增處理函數(shù),使用 useCallback 緩存起來
          const clickHandler = useCallback(() => {
          console.log("hello click");
          }, []);

          useCallback?的原理主要是在掛載的時(shí)候,將定義的 callback 函數(shù)及 deps 依賴掛載該 hook 的 memoizedState,當(dāng)更新時(shí),將依賴進(jìn)行對(duì)比,如果依賴沒變,則直接返回老的 callback 函數(shù),否則則更新新的 callback 函數(shù)及依賴:

          // 掛載時(shí)
          function mountCallback(callback, deps) {
          var hook = mountWorkInProgressHook();
          var nextDeps = deps === undefined ? null : deps;
          hook.memoizedState = [callback, nextDeps];
          return callback;
          }

          // 更新時(shí)
          function updateCallback(callback, deps) {
          var hook = updateWorkInProgressHook();
          var nextDeps = deps === undefined ? null : deps;
          var prevState = hook.memoizedState;

          if (prevState !== null) {
          if (nextDeps !== null) {
          var prevDeps = prevState[1];

          // 如果依賴未變,則直接返回老的函數(shù)
          if (areHookInputsEqual(nextDeps, prevDeps)) {
          return prevState[0];
          }
          }
          }
          // 否則更新新的 callback 函數(shù)
          hook.memoizedState = [callback, nextDeps];
          return callback;
          }

          看起來好像是沒問題了,但是如果我們?cè)趧偛?callback 函數(shù)中使用了 count 這個(gè) state 值呢?

          // 新增處理函數(shù),使用 useCallback 緩存起來
          // 在 callback 函數(shù)中使用 count
          const clickHandler = useCallback(() => {
          console.log("count: ", count);
          }, []);

          當(dāng)我們點(diǎn)擊了幾次計(jì)數(shù),然后再點(diǎn)擊?Hello?組件時(shí),會(huì)發(fā)現(xiàn)我們打印的 count 還是掛載時(shí)候的值,而不是最新的 count 值。其實(shí),這都是是閉包惹得禍(具體解釋可參考:Be Aware of Stale Closures when Using React Hooks)。所以為了讓 callback 函數(shù)中可以使用最新的 state,我們還要將該 state 放入 deps 依賴,但是這樣依賴更新了,callback 函數(shù)也將會(huì)更新,于是?Hello?組件又將會(huì) re-render,這又回到了從前。

          // 新增處理函數(shù),使用 useCallback 緩存起來
          // 在 callback 函數(shù)中使用 count
          // 并將 count 添加進(jìn)依賴
          // 只要 count 更新,callback 函數(shù)又將更新,useCallback 就沒什么用了
          const clickHandler = useCallback(() => {
          console.log("count: ", count);
          }, [count]);

          這樣我們得出了一個(gè)結(jié)論:當(dāng) callback 函數(shù)需要使用 state 值時(shí),如果是 state 值更新引起的更新,useCallback 其實(shí)是沒有任何效果的。

          2.3.2、useRef & useEffect

          為了解決剛才的?useCallback?的閉包問題,我們換一個(gè)方式,引入?useRef?和?useEffect?來解決該問題:

          const App = ({ children }) => {
          console.log("counter render");
          const [count, addCount] = useState(0);

          // 1、創(chuàng)建一個(gè) countRef
          const countRef = useRef(count);

          // 2、依賴改成 countRef
          // 淺對(duì)比 countRef 時(shí),將不會(huì)引起 callback 函數(shù)更新
          // callback 函數(shù)又中可以讀取到 countRef.current 值,即 count 的最新值
          const clickHandler = useCallback(() => {
          console.log("count: ", countRef.current);
          }, [countRef]);

          // 3、當(dāng) count 更新時(shí),更新 countRef 的值
          useEffect(() => {
          countRef.current = count;
          }, [count]);

          return (
          <div className="counter">
          <Hello name="react" onClick={clickHandler} />
          <div className="counter-num">{count}div>

          <button
          onClick={() =>
          {
          addCount(count + 1);
          }}
          >
          add
          button>
          div>
          );
          };

          該方案總結(jié)如下:

          • 通過?useRef?來保存變化的值;

          • 通過?useEffect?來更新變化的值;

          • 通過?useCallback?來返回固定的 callback。


          useRef?保存值的原理如下:

          // 掛載 ref
          function mountRef(initialValue) {
          var hook = mountWorkInProgressHook();

          // 創(chuàng)建一個(gè) ref 對(duì)象,將值掛在 current 屬性上
          var ref = {
          current: initialValue
          };

          {
          Object.seal(ref);
          }

          // 將 ref 掛到 hook 的 memoizedState 屬性上,并返回
          hook.memoizedState = ref;
          return ref;
          }

          // 更新 ref
          function updateRef(initialValue) {
          var hook = updateWorkInProgressHook();
          return hook.memoizedState; // 直接返回 ref
          }

          PS:注意不要跟 hooks API 中的 React.useMemo 搞混,這是兩個(gè)完全不一樣的東西。

          3、context 更新,引起的 re-render

          其實(shí)關(guān)于 context,我們平時(shí)都有在用,如 react-redux,react-router 都運(yùn)用了 context 來進(jìn)行狀態(tài)管理。

          其涉及內(nèi)容比較多也比較復(fù)雜,一時(shí)半會(huì)也說不清,這里就直接推薦一篇文章吧:React Context 源碼淺析

          參考資料



          往期推薦


          2021 TWeb 騰訊前端技術(shù)大會(huì)精彩回顧(附PPT)
          面試題:說說事件循環(huán)機(jī)制(滿分答案來了)
          專心工作只想搞錢的前端女程序員的2020

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野结衣国产区42部 | 天天操天天撸视频免费 | 日本 波多野结衣 影片 | 激情婷婷丁香 | 五月天国产视频乱码免费 |