首页 > 其他分享 >useMemo依赖没变,回调还会反复执行?

useMemo依赖没变,回调还会反复执行?

时间:2023-11-28 14:28:48浏览次数:34  
标签:lazy 依赖 render useMemo 更新 React LazyComponent 回调

经常使用React都知道,有些hook被设计为:依赖项数组 + 回调的形式,比如:

  • useEffect
  • useMemo

通常来说,当依赖项数组中某些值变化后,回调会重新执行。

React的写法十分灵活,那么有没有可能,在依赖项数组不变的情况下,回调依然重新执行?

描述下 Demo

在这个示例中,存在两个文件:

  • App.tsx
  • Lazy.tsx

App.tsx中,会通过React.lazy的形式懒加载Lazy.tsx导出的组件:

// 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导出的LazyComponent大体代码如下:

// Lazy.tsx

function LazyComponent() {
  const ChildComponent = useMemo(() => {
    // ...省略逻辑
  }, []);

  return ChildComponent;
}

export default LazyComponent;

可以发现,LazyComponent组件的子组件是useMemo的返回值,而这个useMemo的依赖项是[](没有依赖项),理论上来说useMemo的回调只会执行一次。

再来看看useMemo回调中的详细代码:

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

  return (
    <Suspense fallback={<div>内层加载...</div>}>
      <LazyCpn />
    </Suspense>
  );
}, []);

简单来说,useMemo会返回一个被 Suspense 包裹的懒加载组件

是不是看起来比较绕,没关系,看看整个Demo的结构图:

  • 整个应用有两层Suspense,两层React.lazy
  • 第二层SuspenseuseMemeo回调的返回值

这里是在线 Demo 地址

应用渲染的结果如下:

现在问题来了,如果在useMemo回调中打印个log,记录下执行情况,那么log会打印多少次?

const ChildComponent = useMemo(() => {
  console.log("useMemo回调执行啦");
  // ...省略代码
}, []);

再次重申,这个useMemo的依赖项是不会变的

在电脑中,log大概会打印 4000 ~ 6000 次,也就是说,useMemo回调会执行 4000 ~ 6000 次,即使依赖不变。

原理分析

首先,要明确一点:hook 依赖项变化,回调重新执行是针对不同更新来说的。

DemouseMemo回调虽然会执行几千次,但他们都是同一次更新中执行的。

如果你对这一点有疑问,可以在LazyComponent(也就是Demo中的第一层React.lazy)中增加 2 个log

  • 一个在useEffect回调中
  • 一个在LazyComponent render函数中
function LazyComponent() {
  console.log("LazyComponent render");

  useEffect(() => {
    console.log("LazyComponent mount");
  }, []);

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

  return ChildComponent;
}

会发现:

  • LazyComponent render执行次数和useMemo回调执行啦一致(都是几千次)
  • LazyComponent mount只会执行一次

也就是说,LazyComponent组件会render几千次,但只会首屏渲染一次。

hook 依赖项变化,回调重新执行这条规则,只适用于不同更新之间(比如首屏渲染再次更新之间),不适用于同一次更新的不同render之间(比如Demo中是首屏渲染的几千次render)。

搞明白上面这些,还得解答一个问题:为啥首屏渲染LazyComponent组件会render几千次?

unwind 机制

在正常情况下,一次更新,同一个组件只会render一次。但还有两种情况,一次更新同一个组件可能render多次:

情况 1 并发更新

在并发更新下,存在低优先级更新进行到中途,被高优先级更新打断的情况,这种情况下,同一个组件可能经历 2 次更新:

  • 低优先级更新(被打断)
  • 高优先级更新(没打断)

Demorender几千次,显然不属于这种情况。

情况 2 unwind 情况

React中,有一类组件,在render时是不能确定渲染内容的,比如:

  • Error Boundray
  • Suspense

对于Error Boundray,在render进行到Error Boundray时,React不知道是否应该渲染报错对应的 UI,只有继续遍历Error Boundray的子孙组件,遇到了报错,才知道最近的Error Boundray需要渲染成报错对应的 UI

比如,对于下述组件结构:

<ErrorBoundary>
  <a>
    <b />
  </a>
</ErrorBoundary>

更新进行到ErrorBoundary时,是不知道是否应该渲染报错对应的 UI,只有继续遍历AB,报错以后,才知道ErrorBoundary需要渲染成报错对应的 UI

同理,对于下述组件结构:

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

更新进行到Suspense时,是不知道是否应该渲染fallback 对应的 UI,只有继续遍历AB,发生挂起后,才知道Suspense需要渲染成fallback 对应的 UI

对于上述两种情况,React中存在一种在同一个更新中的回溯,重试机制,被称为unwind流程。

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

unwind流程是如何进行的呢?以下述代码为例:

<ErrorBoundary>
  <a>
    <b />
  </a>
</ErrorBoundary>

正常更新流程是:

假设B render时抛出错误,则会从B往上回到最近的ErrorBoundary

再重新往下更新:

其中,从 B 回到 ErrorBoundary(途中红色路径)就是unwind流程。

Demo 情况详解

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

首先,首屏渲染遇到第一个React.lazy,开始请求Lazy.tsx的代码:

更新无法继续下去(Lazy.tsx代码还没请求回),进入unwind流程,回到Suspense

Suspense再重新往下更新,进入fallback(即<div>外层加载...</div>)的渲染流程:

所以页面首屏渲染会显示<div>外层加载...</div>

React.lazy请求回Lazy.tsx代码后,开启新的更新流程:

当再次遇到React.lazy(请求<div>子组件</div>代码),又会进入unwind流程。

但是内层的React.lazy与外层的React.lazy是不一样的,外层的React.lazy是在模块中定义的:

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

内层的React.lazy是在useMemo回调中定义的:

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

  return (
    <Suspense fallback={<div>内层加载...</div>}>
      <LazyCpn />
    </Suspense>
  );
}, []);

前者的引用是稳定的,而后者每次执行useMemo回调都会生成新的引用。

这意味着当unwind进入Suspense,重新往下更新,更新进入到LazyComponent后,useMemo回调执行,创建新的React.lazy,又会进入unwind流程:

在同一个更新中,上图蓝色、红色流程会循环出现上千次,直到命中边界情况停止循环。

相对应的,useMemo即使依赖不变,也会在一次更新中执行上千次。

总结

hook 依赖项变化,回调重新执行是针对不同更新来说的。

在某些会触发unwind的场景(比如SuspenseError Boundary)下,一次更新会重复执行很多次。

在这种情况下,即使hook依赖没变,回调也会重新执行。因为,这是同一次更新的反复执行,而不是执行了不同更新。

标签:lazy,依赖,render,useMemo,更新,React,LazyComponent,回调
From: https://www.cnblogs.com/wp-leonard/p/17861849.html

相关文章

  • simulink回调函数在embedded code/autosar的应用
    simulink开发嵌入式方向,在生成的代码中会以注释的形式记录代码生成的时间于模型版本。但编译完成后的可执行文件中并不会存储这些信息,在某些情况下定位问题与确认模型的版本就不容易实现。因此在模型中创建一个全局变量用来存储版本信息,使用回调函数自动填写相关信息。如下图使......
  • .net 依赖注入 基本原理学习
    实例化带参数类如果一个类,在初始化时需要带一个参数,则在注册时使用AddScoped、AddTransient、AddSingleton的回调函数。services.AddScoped<IConfigService>(s=>newTxtConfigServer(){FileName="mail.ini"});使用扩展方法注册在注册时需要使用AddScoped、AddTransient、......
  • .net 依赖注入“传染性”
    .net依赖注入使用的是构造函数注入方式,并且具有传染性。比如有一个控制器中使用了日志和存储两个类,而存储中使用了日志类和配置类,则都只需要在其构造函数中写需要的类,然后在容器中注册,就可以直接使用。日志类代码//日志接口publicinterfaceILog{publicvoidLog(str......
  • .net 控制反转(IoC)和依赖注入(DI)
    引言控制反转(IoC)实现方法:(隐式)依赖注入:需要什么服务(类),直接在类里面写,然后系统在创建类的时候给服务(类)自动赋值。(显式)服务定位器:需要什么服务(类)在给服务定位器要什么服务(类)1.初始化使用引用包:Microsoft.Extensions.DependencyInjection首先需要创建一个容器:ServiceCollec......
  • 2023 合肥站 热身赛 B Problem F. Flower’s Land 换根dp 依赖背包
    传送门。求出包含某个点连通块大小为K的权值和最大值。钦定1为根节点,只求根节点的答案,其实是一个依赖性01背包问题可以\(nk\)的时间内解决。考虑进行换根操作,由于背包是取max的背包没办法进行背包的删除,然而取前后缀背包背包的合并为\(k^2\)复杂度过高。当时还有一个想法是点......
  • Kylin系统下离线安装依赖包
    一、离线安装Kylin依赖包我们由电脑上通过apt-getinstall所安装的包都会下载到/var/cache/apt/archives目录下,可以对所需要安装的依赖包进行抽取,使用以下命令:$sudoapt-get-dinstall<包名>#只下载不安装下载完成后,进入到/var/cache/apt/archives目录下拷贝出来,放到......
  • C++回调函数的定义和调用
    文章目录一、C++回调函数1.C/C++回调函数2.普通回调3.函数指针4.C++类的静态函数作为回调函数5.类的非静态函数作为回调函数6.Lambda表达式作为回调函数7.std::funtion和std::bind的使用二、其他参考资料 一、C++回调函数C++回调函数1.C/C++回调......
  • Angular 依赖注入领域里 optional constructor parameters 的概念介绍
    Angular依赖注入(DI)是一个强大且灵活的设计模式,它可以帮助我们更好地管理和组织我们的代码。构造函数参数的可选性(Optional)是AngularDI系统的一个重要特性。这种特性允许我们将某些服务或值作为可选依赖注入到组件或服务中,这样,如果这些服务或值不存在,我们的代码仍然可以正常工......
  • Angular 使用 Constructor Parameters 进行依赖注入的优缺点
    构造函数参数(ConstructorParameters)在Angular中是一种进行依赖注入(DependencyInjection)的重要方式之一。依赖注入是一种设计模式,通过该模式,一个类的依赖关系不是在类内部直接创建,而是通过外部提供这些依赖关系。在Angular中,依赖注入通过注入器(Injector)来实现,而构造函数参数是一......
  • 常用的maven dependency依赖
    <dependencies><!--junit--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope><......