大家好,赖没我卡颂。调还
经常使用React的复执同学都知道,有些hook被设计为:「依赖项数组 + 回调」的赖没形式,比如:
通常来说,调还当「依赖项数组」中某些值变化后,复执回调会重新执行。赖没
我们知道,调还React的复执写法十分灵活,那么有没有可能,在「依赖项数组」不变的情况下,回调依然重新执行?
本文就来探讨一个这样的场景。
在这个示例中,存在两个文件:
在App.tsx中,会通过React.lazy的形式懒加载Lazy.tsx导出的组件:
// App.tsximport { 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.tsxfunction 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的结构图:
这里是在线Demo地址[1]
应用渲染的结果如下:
现在问题来了,如果我们在useMemo回调中打印个log,记录下执行情况,那么log会打印多少次?
const ChildComponent = useMemo(() => { console.log("useMemo回调执行啦") // ...省略代码}, []);
再次重申,这个useMemo的依赖项是不会变的
在我的电脑中,log大概会打印4000~6000次,也就是说,useMemo回调会执行4000~6000次,即使依赖不变。
why?
首先,我们要明确一点:「hook依赖项变化,回调重新执行」是针对不同更新来说的。
而我们的Demo中useMemo回调虽然会执行几千次,但他们都是同一次更新中执行的。
如果你对这一点有疑问,可以在LazyComponent(也就是Demo中的第一层React.lazy)中增加2个log:
function LazyComponent() { console.log("LazyComponent render") useEffect(() => { console.log("LazyComponent mount"); }, []); const ChildComponent = useMemo(() => { // ...省略逻辑 }, []); return ChildComponent;}
会发现:
也就是说,LazyComponent组件会render几千次,但只会首屏渲染一次。
而「hook依赖项变化,回调重新执行」这条规则,只适用于不同更新之间(比如「首屏渲染」和「再次更新」之间),不适用于同一次更新的不同render之间(比如Demo中是首屏渲染的几千次render)。
搞明白上面这些,我们还得解答一个问题:为啥首屏渲染LazyComponent组件会render几千次?
在正常情况下,一次更新,同一个组件只会render一次。但还有两种情况,一次更新同一个组件可能render多次:
在并发更新下,存在「低优先级更新进行到中途,被高优先级更新打断」的情况,这种情况下,同一个组件可能经历2次更新:
在Demo中render几千次,显然不属于这种情况。
在React中,有一类组件,在render时是不能确定渲染内容的,比如:
对于Error Boundray,在render进行到Error Boundray时,React不知道是否应该渲染「报错对应的UI」,只有继续遍历Error Boundray的子孙组件,遇到了报错,才知道最近的Error Boundray需要渲染成「报错对应的UI」。
比如,对于下述组件结构:
<ErrorBoundary> <A> <B/> </A></ErrorBoundary>
更新进行到ErrorBoundary时,是不知道是否应该渲染「报错对应的UI」,只有继续遍历A、B,报错以后,才知道ErrorBoundary需要渲染成「报错对应的UI」。
同理,对于下述组件结构:
<Suspense fallback={ <div>加载...</div>}> <A> <B/> </A></Suspense>
更新进行到Suspense时,是不知道是否应该渲染「fallback对应的UI」,只有继续遍历A、B,发生挂起后,才知道Suspense需要渲染成「fallback对应的UI」。
对于上述两种情况,React中存在一种「在同一个更新中的回溯,重试机制」,被称为unwind流程。
在Demo中,就是遭遇了上千次的unwind。
那unwind流程是如何进行的呢?以下述代码为例:
<ErrorBoundary> <A> <B/> </A></ErrorBoundary>
正常更新流程是:
假设B render时抛出错误,则会从B往上回到最近的ErrorBoundary:
再重新往下更新:
其中,「从B回到ErrorBoundary」(途中红色路径)就是unwind流程。
在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.tsxconst 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的场景(比如Suspense、Error Boundary)下,一次更新会重复执行很多次。
在这种情况下,即使hook依赖没变,回调也会重新执行。因为,这是同一次更新的反复执行,而不是执行了不同更新。
[1]在线Demo地址:https://codesandbox.io/s/unruffled-nightingale-thzv7z?file=/src/ImportComponent.js。
责任编辑:姜华 来源: 魔术师卡颂 HookuseMemo(责任编辑:百科)
中国医疗集团(08225.HK)发布公告:预计年度税后纯利大幅增加不少于100%
开源! 基于lowcode行业的开源CMS系统,轻松帮助企业和个人搭建知识管理系统
你还在用Excel处理数据?Python Pandas让你处理数据事半功倍!