自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

UseMemo依賴沒變,回調(diào)還會反復(fù)執(zhí)行?

開發(fā) 前端
首先,我們要明確一點(diǎn):「Hook依賴項(xiàng)變化,回調(diào)重新執(zhí)行」是針對不同更新來說的。而我們的Demo中UseMemo回調(diào)雖然會執(zhí)行幾千次,但他們都是同一次更新中執(zhí)行的。

大家好,我卡頌。

經(jīng)常使用React的同學(xué)都知道,有些hook被設(shè)計(jì)為:「依賴項(xiàng)數(shù)組 + 回調(diào)」的形式,比如:

  • useEffect
  • useMemo

通常來說,當(dāng)「依賴項(xiàng)數(shù)組」中某些值變化后,回調(diào)會重新執(zhí)行。

我們知道,React的寫法十分靈活,那么有沒有可能,在「依賴項(xiàng)數(shù)組」不變的情況下,回調(diào)依然重新執(zhí)行?

本文就來探討一個這樣的場景。

描述下Demo

在這個示例中,存在兩個文件:

  • App.tsx
  • Lazy.tsx

在App.tsx中,會通過React.lazy的形式懶加載Lazy.tsx導(dǎo)出的組件:

// App.tsx

import { Suspense, lazy } from "react";

const LazyCpn = lazy(() => import("./Lazy"));

function App() {
  return (
    <Suspense fallback={<div>外層加載...</div>}>
      <LazyCpn />
    </Suspense>
  );
}

export default App;

Lazy.tsx導(dǎo)出的LazyComponent大體代碼如下:

// Lazy.tsx

function LazyComponent() {

  const ChildComponent = useMemo(() => {
   // ...省略邏輯
  }, []);

  return ChildComponent;
}

export default LazyComponent;

可以發(fā)現(xiàn),LazyComponent組件的子組件是useMemo的返回值,而這個useMemo的依賴項(xiàng)是[](沒有依賴項(xiàng)),理論上來說useMemo的回調(diào)只會執(zhí)行一次。

再來看看useMemo回調(diào)中的詳細(xì)代碼:

const ChildComponent = useMemo(() => {
  const LazyCpn = lazy(
    () => Promise.resolve({ default: () => <div>子組件</div>})
  )

  return (
    <Suspense fallback={<div>內(nèi)層加載...</div>}>
      <LazyCpn />
    </Suspense>  
  );
}, []);

簡單來說,useMemo會返回一個「被Suspense包裹的懶加載組件」。

是不是看起來比較繞,沒關(guān)系,我們看看整個Demo的結(jié)構(gòu)圖:

  • 整個應(yīng)用有兩層Suspense,兩層React.lazy。
  • 第二層Suspense是useMemeo回調(diào)的返回值。

這里是在線Demo地址[1]

應(yīng)用渲染的結(jié)果如下:

現(xiàn)在問題來了,如果我們在useMemo回調(diào)中打印個log,記錄下執(zhí)行情況,那么log會打印多少次?

const ChildComponent = useMemo(() => {
  console.log("useMemo回調(diào)執(zhí)行啦")
  // ...省略代碼
}, []);

再次重申,這個useMemo的依賴項(xiàng)是不會變的

在我的電腦中,log大概會打印4000~6000次,也就是說,useMemo回調(diào)會執(zhí)行4000~6000次,即使依賴不變。

why?

原理分析

首先,我們要明確一點(diǎn):「hook依賴項(xiàng)變化,回調(diào)重新執(zhí)行」是針對不同更新來說的。

而我們的Demo中useMemo回調(diào)雖然會執(zhí)行幾千次,但他們都是同一次更新中執(zhí)行的。

如果你對這一點(diǎn)有疑問,可以在LazyComponent(也就是Demo中的第一層React.lazy)中增加2個log:

  • 一個在useEffect回調(diào)中。
  • 一個在LazyComponent render函數(shù)中。
function LazyComponent() {
  console.log("LazyComponent render")
  
  useEffect(() => {
    console.log("LazyComponent mount");
  }, []);

  const ChildComponent = useMemo(() => {
   // ...省略邏輯
  }, []);

  return ChildComponent;
}

會發(fā)現(xiàn):

  • LazyComponent render執(zhí)行次數(shù)和useMemo回調(diào)執(zhí)行啦一致(都是幾千次)
  • LazyComponent mount只會執(zhí)行一次

也就是說,LazyComponent組件會render幾千次,但只會首屏渲染一次。

而「hook依賴項(xiàng)變化,回調(diào)重新執(zhí)行」這條規(guī)則,只適用于不同更新之間(比如「首屏渲染」和「再次更新」之間),不適用于同一次更新的不同render之間(比如Demo中是首屏渲染的幾千次render)。

搞明白上面這些,我們還得解答一個問題:為啥首屏渲染LazyComponent組件會render幾千次?

unwind機(jī)制

在正常情況下,一次更新,同一個組件只會render一次。但還有兩種情況,一次更新同一個組件可能render多次:

情況1 并發(fā)更新

在并發(fā)更新下,存在「低優(yōu)先級更新進(jìn)行到中途,被高優(yōu)先級更新打斷」的情況,這種情況下,同一個組件可能經(jīng)歷2次更新:

  • 低優(yōu)先級更新(被打斷)
  • 高優(yōu)先級更新(沒打斷)

在Demo中render幾千次,顯然不屬于這種情況。

情況2 unwind情況

在React中,有一類組件,在render時是不能確定渲染內(nèi)容的,比如:

  • Error Boundray
  • Suspense

對于Error Boundray,在render進(jìn)行到Error Boundray時,React不知道是否應(yīng)該渲染「報錯對應(yīng)的UI」,只有繼續(xù)遍歷Error Boundray的子孫組件,遇到了報錯,才知道最近的Error Boundray需要渲染成「報錯對應(yīng)的UI」。

比如,對于下述組件結(jié)構(gòu):

<ErrorBoundary>
  <A>
    <B/>
  </A>
</ErrorBoundary>

更新進(jìn)行到ErrorBoundary時,是不知道是否應(yīng)該渲染「報錯對應(yīng)的UI」,只有繼續(xù)遍歷A、B,報錯以后,才知道ErrorBoundary需要渲染成「報錯對應(yīng)的UI」。

同理,對于下述組件結(jié)構(gòu):

<Suspense fallback={<div>加載...</div>}>
  <A>
    <B/>
  </A>
</Suspense>

更新進(jìn)行到Suspense時,是不知道是否應(yīng)該渲染「fallback對應(yīng)的UI」,只有繼續(xù)遍歷A、B,發(fā)生掛起后,才知道Suspense需要渲染成「fallback對應(yīng)的UI」。

對于上述兩種情況,React中存在一種「在同一個更新中的回溯,重試機(jī)制」,被稱為unwind流程。

在Demo中,就是遭遇了上千次的unwind。

那unwind流程是如何進(jìn)行的呢?以下述代碼為例:

<ErrorBoundary>
  <A>
    <B/>
  </A>
</ErrorBoundary>

正常更新流程是:

假設(shè)B render時拋出錯誤,則會從B往上回到最近的ErrorBoundary:

再重新往下更新:

其中,「從B回到ErrorBoundary」(途中紅色路徑)就是unwind流程。

Demo情況詳解

在Demo中完整的更新流程如下:

首先,首屏渲染遇到第一個React.lazy,開始請求Lazy.tsx的代碼:

更新無法繼續(xù)下去(Lazy.tsx代碼還沒請求回),進(jìn)入unwind流程,回到Suspense:

Suspense再重新往下更新,進(jìn)入fallback(即<div>外層加載...</div>)的渲染流程:

所以頁面首屏渲染會顯示<div>外層加載...</div>。

當(dāng)React.lazy請求回Lazy.tsx代碼后,開啟新的更新流程:

當(dāng)再次遇到React.lazy(請求<div>子組件</div>代碼),又會進(jìn)入unwind流程。

但是內(nèi)層的React.lazy與外層的React.lazy是不一樣的,外層的React.lazy是在模塊中定義的:

// App.tsx
const LazyCpn = lazy(() => import("./Lazy"));

內(nèi)層的React.lazy是在useMemo回調(diào)中定義的:

const ChildComponent = useMemo(() => {
  const LazyCpn = lazy(
    () => Promise.resolve({ default: () => <div>子組件</div>})
  )

  return (
    <Suspense fallback={<div>內(nèi)層加載...</div>}>
      <LazyCpn />
    </Suspense>  
  );
}, []);

前者的引用是穩(wěn)定的,而后者每次執(zhí)行useMemo回調(diào)都會生成新的引用。

這意味著當(dāng)unwind進(jìn)入Suspense,重新往下更新,更新進(jìn)入到LazyComponent后,useMemo回調(diào)執(zhí)行,創(chuàng)建新的React.lazy,又會進(jìn)入unwind流程:

在同一個更新中,上圖藍(lán)色、紅色流程會循環(huán)出現(xiàn)上千次,直到命中邊界情況停止循環(huán)。

相對應(yīng)的,useMemo即使依賴不變,也會在一次更新中執(zhí)行上千次。

總結(jié)

「hook依賴項(xiàng)變化,回調(diào)重新執(zhí)行」是針對不同更新來說的。

在某些會觸發(fā)unwind的場景(比如Suspense、Error Boundary)下,一次更新會重復(fù)執(zhí)行很多次。

在這種情況下,即使hook依賴沒變,回調(diào)也會重新執(zhí)行。因?yàn)椋@是同一次更新的反復(fù)執(zhí)行,而不是執(zhí)行了不同更新。

參考資料

[1]在線Demo地址:https://codesandbox.io/s/unruffled-nightingale-thzv7z?file=/src/ImportComponent.js。

責(zé)任編輯:姜華 來源: 魔術(shù)師卡頌
相關(guān)推薦

2022-12-13 08:36:42

D-SMARTOracle數(shù)據(jù)庫

2012-02-01 10:33:59

Java

2022-06-08 08:01:20

useEffect數(shù)組函數(shù)

2011-07-25 14:32:40

Cocoa 框架 函數(shù)

2011-06-15 11:05:14

C語言回調(diào)函數(shù)

2017-11-16 16:15:28

Await開發(fā)嵌套

2022-04-12 08:30:52

回調(diào)函數(shù)代碼調(diào)試

2019-11-05 10:03:08

callback回調(diào)函數(shù)javascript

2009-08-21 17:02:20

ASP.NET異步回調(diào)

2009-12-22 19:00:08

WCF回調(diào)

2009-08-12 10:11:18

C# 回調(diào)函數(shù)

2023-11-10 16:31:31

2015-10-26 09:25:42

2009-08-19 16:40:35

C#回調(diào)

2011-05-20 17:19:25

回調(diào)函數(shù)

2024-06-03 08:32:54

2009-08-19 17:10:09

C#回調(diào)函數(shù)

2009-11-04 11:32:20

VB.NET回調(diào)函數(shù)

2010-02-04 16:07:39

C++回調(diào)函數(shù)

2018-11-29 08:00:20

JavaScript異步Promise
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號