useCallback 帶來的隱式依賴問題


案例

// 新建文章組件
function EditArticle() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [other, setOther] = useState("");

  // 獲取當前「標題」和「內容」的長度
  const getTextLen = () => {
    return [title.length, content.length];
  };

  // 上報當前「標題」和「內容」的長度
  const report = () => {
    const [titleLen, contentLen] = getTextLen();
    if (contentLen > 0) {
      console.log(`埋點 >>> 標題長度 ${titleLen}, 內容長度 ${contentLen}`);
    }
  };

  /**
   * 副作用
   * 當「標題」長度變化時,上報
   */
  useEffect(() => {
    report();
  }, [title]);

  return (
    <div className="App">
       文章標題   <input value={title} onChange={(e) => setTitle(e.target.value)} />
       
       文章內容  <input value={content} onChange={(e) => setContent(e.target.value)} />
      
       其他不相關狀態: <input value={other} onChange={(e) => setOther(e.target.value)} />

      <MemoArticleTypeSetting getTextLen={getTextLen} />
    </div>
  );
}
enum ArticleType {
  WEB = "前端",
  SERVER = "后端",
}

// 子組件,修改文章類型(無需關注,它只是接受了父組件的一個參數而已)
const ArticleTypeSetting: FC<{ getTextLen: () => number[] }> = ({  getTextLen }) => {
  console.log(" --- ArticleTypeSetting 組件重新渲染 --- ");

  const [articleType, setArticleType] = useState<ArticleType>(ArticleType.WEB);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setArticleType(e.target.value as ArticleType);

    console.log(  "埋點 >>> 切換類型,當前「標題」和「內容」長度:", getTextLen()  );
  };

  return (
    <div>
      <div>文章類型組件,當選擇類型時上報「標題」和「內容」長度</div>
      <div>
        {[ArticleType.WEB, ArticleType.SERVER].map((type) => (
          <div>  
            <input  type="radio" value={type} checked={articleType === type} onChange={handleChange}  /> 
           {type} 
          </div>
        ))}
      </div>
    </div>
  );
};

const MemoArticleTypeSetting = memo(ArticleTypeSetting);

哪些地方需要優化

  1. 子組件 ArticleTypeSetting 是使用 memo 包裹的,這個組件是希望盡可能的減少渲染次數的(假裝這個組件有性能問題,一般不用包)。但是,現在每當修改任意一個值(如 other),子組件都會重新渲染,這顯然是沒有達到優化的預期的。
  2. 這里不規范, useEffect 中使用了 report 函數,但是沒有將它放到依賴數組中。

對代碼進行了一些修改

// 獲取當前「標題」和「內容」的長度
  const getTextLen = useCallback(() => {
    return [title.length, content.length];
  }, [title, content]);

  // 上報當前「標題」和「內容」的長度
  const report = useCallback(() => {
    const [titleLen, contentLen] = getTextLen();
    if (contentLen > 0) {
      console.log(`埋點 >>> 內容長度 ${titleLen}, 內容長度 ${contentLen}`);
    }
  }, [getTextLen]);

  /**
   * 副作用
   * 當「標題」長度變化時,上報
   */
  useEffect(() => {
    report();
  }, [title, report]);

還有問題

當 「文章內容」修改了之后,會觸發 useEffect 繼續上報

為什么

初衷只是使用 useCallback 避免頻繁調用,但當一個 useCallback 的依賴項變化后,這個 useEffect 會被執行,就像上面修改過后的代碼一樣,「文章內容」修改了之后,也會觸發 useEffect 的,這就是「useCallback 帶來的隱式依賴問題」。

如何解決

將 函數綁定到 useRef 上來解決

const getTextLenRef = useRef<() => [number, number]>(() => [0, 0]);

  // 獲取當前「標題」和「內容」的長度
  getTextLenRef.current = () => {
    return [title.length, content.length];
  };

  // 上報當前「標題」和「內容」的長度
  const report = () => {
    const [titleLen, contentLen] = getTextLenRef.current();
    if (contentLen > 0) {
      console.log(`埋點 >>> 標題長度 ${titleLen}, 內容長度 ${contentLen}`);
    }
  };

  /**
   * 副作用
   * 當「標題」長度變化時,上報
   */
  useEffect(() => {
    report();
  }, [title]);

  將函數綁定到 Ref上,ref 引用不論怎樣都保持不變,而且函數每次 render ref 上又會綁定最新的函數,不會有閉包問題。我在開發一個復雜項目中,大量的使用了這種方式,這讓我的開發效率提升。它讓我專注於寫業務,而不是專注於解決閉包問題。

思考我們最開始使用 useCallback 的理由

只有「需要保存一個函數閉包結果,如配合 debounce、throttle 使用」這個是真正需要使用 useCallback 的,其他的都可能帶來風險

在絕大多數情況下,開發者想要的僅僅只是避免函數的引用變化而已,而 useCallback 帶來的隱式依賴問題會給你帶來很大的麻煩,所以推薦使用 useRefCallback,把函數掛到 ref 上,這樣代碼更不容易留下隱患或帶來問題,還可以省去維護 useCallback 依賴項的精力

https://github.com/huyaocode/webKnowledge/issues/12


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM