再聊react hook

React Hook是React函數(shù)式組件,它不僅僅有函數(shù)組件的特性,還帶有React框架的特性。所以,官網(wǎng)文檔多次強(qiáng)調(diào):

只在 React 函數(shù)中調(diào)用 Hook

不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook。你可以:

  • ? 在 React 的函數(shù)組件中調(diào)用 Hook
  • ? 在自定義 Hook 中調(diào)用其他 Hook

1. 那么 React 中 Function Component 與 Class Component 有何不同?

Class Component:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert("Followed " + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

Function Component:

function ProfilePage(props) {
  const showMessage = () => {
    alert("Followed " + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return <button onClick={handleClick}>Follow</button>;
}

如下父級(jí)組件的調(diào)用方式:

<ProfilePageFunction user={this.state.user} />
<ProfilePageClass user={this.state.user} />

場(chǎng)景:那么當(dāng)點(diǎn)擊按鈕后的 3 秒內(nèi),父級(jí)修改了 this.state.user,彈出的用戶(hù)名是修改前的還是修改后的呢?
答案:Class Component 展示的是修改后的值,F(xiàn)unction Component 展示的是修改前的值
原因this 在 Class Component 中是可變的,當(dāng)組件入?yún)l(fā)生變化時(shí), this.props同步改變。而 Function Component 不存在this.props 的語(yǔ)法,因此 props 總是不可變的。

2. 用Hook 創(chuàng)建函數(shù)組件,會(huì)有什么變化呢?

import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
 
 function Example() {
   const [count, setCount] = useState(0);
 
   const handleAlertClick = useCallback(()=>{
     setTimeout(() => {
       alert('You clicked on: ' + count);
     }, 3000)
   }, [count]);
 
   return (
     <div>
       <p>You clicked {count} times</p>
       <button onClick={() => setCount(count + 1)}>
         增加 count
       </button>
       <button onClick={handleAlertClick}>
         顯示 count
       </button>
     </div>
   );
 }
 const rootElement = document.getElementById("root");
 ReactDOM.render(<Example />, rootElement);

場(chǎng)景:點(diǎn)擊 顯示 按鈕,在 3s 后(模擬耗時(shí)任務(wù))會(huì)出現(xiàn)彈層。在這 3s 期間快速點(diǎn)擊 增加 count 按鈕
結(jié)果:3s 后看到的彈層計(jì)數(shù)仍舊為 0!
原因:在 handleAlertClick 函數(shù)執(zhí)行的那個(gè) Render 過(guò)程里,count 的值可以看作常量 。執(zhí)行 setCount(count + 1) 時(shí)會(huì)交由一個(gè)全新的 Render 渲染,所以不會(huì)執(zhí)行 handleAlertClick 函數(shù)。

那我們用useEffect 改造一下呢?

 const [count, setCount] = useState(0);
 useEffect(()=>{
    setTimeout(() => {
      alert('count: ' + count);
    }, 3000)
  }, [count]); // 監(jiān)聽(tīng)count變化

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        增加 count
      </button>
    </div>
  );
}
...

行為:三秒鐘內(nèi)點(diǎn)擊“增加 count”按鈕三次。
結(jié)果:先顯示0(初始化),然依次顯示1,2,3,useEffect被觸發(fā)4次。
原因useEffect也是具有 Capture Value 的特性,每次render都是獨(dú)立快照,拿到的 count 都是固化下來(lái)的常量。和上面例子不同之處在于,它監(jiān)聽(tīng)了count變化,可以被觸發(fā)多次Render。

什么是Capture Value?

每次 Render 的內(nèi)容都會(huì)形成一個(gè)快照并保留下來(lái),因此當(dāng)狀態(tài)變更而 Rerender 時(shí),就形成了 N 個(gè) Render 狀態(tài),而每個(gè) Render 狀態(tài)都擁有自己固定不變的 Props 與 State。

3. 如何繞過(guò) Capture Value?

利用 useRef !

useRef定義

useRef 返回一個(gè)可變的 ref 對(duì)象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對(duì)象在組件的整個(gè)生命周期內(nèi)保持不變。

ref 類(lèi)型的變量通常是用來(lái)存儲(chǔ) DOM 元素引用。

但在 react hooks 中,它可以存放任何可變數(shù)據(jù),并在所有 Render 過(guò)程中保持著唯一引用,因此所有對(duì) ref 的賦值或取值,拿到的都只有一個(gè)最終狀態(tài),而不會(huì)在每個(gè) Render 間存在隔離。

function Example() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  const handleAlertClick = useCallback(
    () => {
      setTimeout(() => {
        alert("You clicked on: " + countRef.current);
      }, 3000);
    },
    [count]
  );

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          countRef.current = count + 1;
          setCount(count + 1);
        }}
      >
        增加 count
      </button>
      <button onClick={handleAlertClick}>顯示 count</button>
    </div>
  );

行為:三秒鐘內(nèi)點(diǎn)擊“增加 count”按鈕三次。
結(jié)果: 先顯示0(初始化),3s 后顯示3次3。

4. useEffect搭配useReducer

利用 useEffect 的兄弟 useReducer 函數(shù),可以將更新與動(dòng)作解耦。

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);

這就是一個(gè)局部 “Redux”,由于更新變成了dispatch({ type: "tick" }) 所以不管更新時(shí)需要依賴(lài)多少變量,在調(diào)用更新的動(dòng)作里都不需要依賴(lài)任何變量。 具體更新操作在 reducer 函數(shù)里寫(xiě)就可以了

5. useMemo vs useCallback

按照官網(wǎng)說(shuō)法,兩個(gè)的相同點(diǎn)和不同點(diǎn)非常明確(React教程

  • 相同點(diǎn):
    • 都會(huì)在組件第一次渲染的時(shí)候執(zhí)行
    • 依賴(lài)的變量發(fā)生改變時(shí)再次執(zhí)行
    • 返回緩存的值
  • 不同點(diǎn):
    • useMemo 返回緩存的變量
    • useCallback 返回緩存的函數(shù)

useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)

5.1 useMemo實(shí)例

export function useDocumentMap() {
  const { docState: { sideBarList = [] } = {} } = useStore()
  return useMemo(() => getDocumentMap(sideBarList), [sideBarList])
}

getDocumentMap是一個(gè)復(fù)雜計(jì)算函數(shù),sideBarList為store里存儲(chǔ)的值。UI顯示的時(shí)候,需要做一次model=>UI model的轉(zhuǎn)換。因?yàn)檫@個(gè)轉(zhuǎn)換需要復(fù)雜計(jì)算,所以用useMemo做緩存,在sideBarList不變的情況下,調(diào)用useDocumentMap獲取UI model只會(huì)計(jì)算一次。

5.2 useCallback實(shí)例

import React, { useState, useCallback } from 'react';
import Button from './Button';

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleClickButton1 = () => {
    setCount1(count1 + 1);
  };

  const handleClickButton2 = useCallback(() => {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <div>
      <div>
        <Button onClickButton={handleClickButton1}>Button1</Button>
      </div>
      <div>
        <Button onClickButton={handleClickButton2}>Button2</Button>
      </div>
    </div>
  );
}

// Button組件
import React from 'react';

const Button = ({ onClickButton, children }) => {
  return (
    <>
      <button onClick={onClickButton}>{children}</button>
      <span>{Math.random()}</span>
    </>
  );
};

export default React.memo(Button);

可見(jiàn)Button組件只依賴(lài)onClickButtonchildren。
如果點(diǎn)擊Button1,因?yàn)?code>count1的值發(fā)生變化,導(dǎo)致容器App重新渲染,handleClickButton1變量重新被聲明(又創(chuàng)建了一個(gè)新的函數(shù)對(duì)象),從而第一個(gè)Button被再次渲染。而handleClickButton2呢?因?yàn)楸?code>useCallback包了一層,且依賴(lài)count2,所以,此時(shí)handleClickButton2變量不會(huì)被重新聲明,還是之前的函數(shù)引用地址,所以,第二個(gè)Button不會(huì)被重復(fù)渲染。

React.memo 這個(gè)方法,會(huì)對(duì) props 做一個(gè)淺層比較,如果 props 沒(méi)有發(fā)生改變,則不會(huì)重新渲染此組件

5.3 總結(jié)

  • 如果有函數(shù)傳遞給子組件,使用useCallback
  • 如果有值傳遞給子組件,使用useMemo
  • useMemo、useCallback 都自帶閉包,類(lèi)似useEffectCapture Value能力

參考文章:
理解 React Hooks 的 Capture Value 特性
精讀《useEffect 完全指南》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容