Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • react基础

  • react更新特性

    • react18 更新特性
    • react进阶

    • react
    • react更新特性
    2022-04-19
    目录

    react18 更新特性

    # 01. Automatic Batching(自动更新)

    • 自动批量更新state,减少渲染次数。react的batching 是将多个状态合并成一次重新渲染,如下,组件只会在handleClick被调用后,执行一次渲染。

    生效范围:

    React18之前 React18以后
    react event handler react event handler
    Promise
    setTimeout
    native event handler
    ...
    import React, { useState } from 'react'
    export default function Index() {
        const [count, setCount] = useState(0);
        const [clicked, setClicked] = useState(0);
        const handleClick = () =>{
            setClicked(clicked+1)
            setCount(count+1)
        }
        // 执行两次操作
        const handleClick = () =>{
          setTimeout(() => {
              setClicked(clicked+1)
              setCount(count+1)
          },0)
        }
        return (
            <div>
                <button onClick={handleClick}>点击</button>
                <div>
                    count: {count}
                    clicked: {clicked}
                </div>
            </div>
        )
    }
    
    
    • 不同上下文中,调用多个状态更新,例如在promise,回调或者定时器中,react就不会进行batching操作,不会将两次更新合二为一,会执行两次更新,但实质上 我们可能只需要一次。在react 18中,所有这些用例都会被覆盖,同时会批量更新状态,无论上下文如何,都会自动更新。 这个与unstable_batchedUpdates行为一致 ,react会默认帮我们完成这个操作。
    • 如果不希望进行batch更新操作,可以使用flushSync,它将在每次运行完我们传递给它的函数后,重新渲染组件,当我们按照下面实例方式编写,组件会被渲染两次。
    import { flushSync } from 'react-dom'
    
        const handleClick = () =>{
          flushSync(() => {
              setClicked(clicked+1)
          },0)
                flushSync(() => {
              setCount(count+1)
          },0)
        }
    

    # 02. Concurrent Mode(并发渲染)

    • 渲染模型的变化

    react18之前版本的渲染模型是线性的, 渲染都是一个接着一个被触发,并且还要被触发了就无法终止 。

    react18以后,渲染被分了优先级,开发者决定哪部分渲染是低优先级的,可以被暂定,终中断的,哪部分的渲染是高优先级的,需要在主线程,也就是不可被中断的 react会在内部调度,来保证并发模式下UI的一致性,这样的话,即使有大型渲染任务也不会被卡住UI,可以有更高的用户体验

    • 渲染可以被中断,继续,终止的
    • 渲染可以在后台进行
    • 渲染可以有优先级
    • 不是新功能,而是一种新的底层机制

    # 03. Transitions(过渡)

    • 指定渲染优先级

    Transitions(过渡)告诉react更新的优先级,在Concurrent模式上建立的

    # TransitionsAPI

    startTransition 或者hook 中useTransition

    import { startTransition, useTransition } from 'react';
    // 紧急状态
    setInputValue(value)
    startTransition(() => {
      setSearchQuery(value)
    })
    
    // hook
    
    const [isPending, startTransition] = useTransition();
    // isPending 告知我们这个次优先级的渲染是否在等待中,如果需要标记某些渲染是低优先级的,可以用上述的两个API把state更新操作包裹起来,这样使react内部明确渲染的调度逻辑 
    

    场景:优先响应input的渲染,其次响应列表渲染 我们在搜索输入时更新,我们会期望他的值随着我们的输入变化而变化,而当我们输入时,搜索结果可能在1s内出现,这时我们将输入搜索的内容部分标记为紧急更新,而元素过滤的部分被标记为次要,也就是我们理解的【过渡】。过渡本身可能会被紧急更新中断,而之前不相关的过渡,将被忽略。这将告诉ui仅显示其最新状态,并跳过次要状态的更新,过渡,这个过程中计算可能慢,有时会返回中间值 或不相关的状态。我们下面代码所示,我们将input 框中的值的改变标记为紧急状态. 在过渡期会进行二次更新,因为它可能会触发缓慢更新,同时我们在输入时,会导致ui卡顿或者延迟,影响用户体验 。

    startTransition 非常适应用于任何需要后台运行的更新,例如当遇到缓慢且复杂的渲染任务,或者当更新依赖于数据获取时,网速很慢的情况。

    # 04. Suspense and SSR

    • 更方便的组织并行请求和 loading状态的代码

    # SSR

    不使用SSR渲染,网站在第一次运行会白屏,这是因为浏览器需求请求并读取我们的javascrupt,这需要一些时间,接着才会加载组件,页面才能进行交互,使用SSR时,用户可以直观的看到应用程序外观,但在加载javascript时,依旧无法交互,其工作原理是先在服务器上的渲染所有组件,然后将结果作为HTML发送给浏览器 ,接着像往常一样加载javascript,而html通过激活(Hydration)作用后,恢复可交互的状态,这会将静态html元素转化为动态react组件。这种方式存在的主要问题是,只要javascript还未被获取加载,html就还未激活,页面无法进行交互(interactive)。

    为了解决这个瀑布问题,react18 SSR提供了两个新功能:

    • Streamimg HTML (流式html):简而言之,流式html意味着服务器可以在你的组件渲染时,发送组件的片段,这方式可以通过Suspense实现,我们可以在应用程序中声明哪些部分需要加载更长的时间,哪些部分应该直接渲染。如果我们渲染一篇带有评论的文章,其中文章是页面的关键部分,我们可以直接发送要加载文章内容的html到浏览器,而无需等待评论。我们可以使用Suspense展示一个加载器,一旦评论准备好,react将发送新的HTML替代加载指示器。
    <NavBar />
    <SideBar />
    <Article />
    <Suspense callback={<Loader/>}>
    <Components />
    </Suspense>
    
    • Selective Hydration(选择性激活):完全改变了规则 ,before:必须等待每个组件被渲染后,才能开始激活,从而导致代码分割问题,而现在组件使用Suspense进行包裹,不会再阻碍激活。

    比如上述文章 评论组件使用Suspense包裹,就不会阻碍文章和其他组件被激活。每个准备好的组件都会开始被激活,一旦浏览器获取到需求的内容和javascript代码 ,评论也会被激活。另外特殊一点:组件被完全被激活之前与之交互,比如说单击了某处,react会记录下这次点击事件,并且把点击组件的Hydration优先级提高,因为它更紧迫。当组件hydration 完成后,react会重新触发之前记录的点击事件,让组件响应这个交互(确保了交互在我们激活完成后,再次被执行 保证它优于其他组件执行激活操作 )。如果没有其他高优先级的工作了,react会进行其他组件的Hrdartion部分(优先从组件树中靠前的位置)。

    # Suspense

    过去我们是如何获取数据并渲染的?

    • fetch on render
    • fetch then render
    • fetch whiler render(Suspense)
    # 基本原理

    在suspense包裹的内容,如果react看到一个被throw出来的promise ,那么react会catch住它,并找到离他最近的Suspense组件 ,这样suspense就知道 它需要等这个promise完成 ,接着他就可以直接渲染fallbak的loading部分。在promise.resolve后 ,suspense就会渲染内部需要使用的组件,所以为了使用suspense ,数据请求的方法 是需要符合某种约定的,也就意味着我们的网络请求层 就需要适配React Suspense的这种约定,react希望更多的网络请求库能够适配 suspense这种机制 ,这样开发者就能更方便的使用这种功能,例如Relay, Next.js, Hydrogen, or Remix

    function Component() {
      if (data) {
        return <div>{data.message}</div>
      }
      throw promise
      // react will catch this, find the closest "Suspense" Component
    }
    
    React.createRoot(rootEL).render(
      <React.Suspense callback={<div>Loading...</div>}>
        <Component/>
      </React.Suspense>
    )
    

    # 05. New Client and Server Rendering APIs

    # React DOM 客户端 (opens new window)

    • 新API 导出位置:react-dom/client
    1. createRoot:为root创建以渲染或卸载的新方法,使用它代替ReactDOM.render. 没有它,React 18 中的新功能就无法工作

    2. hydrateRoot: 为hydrate一个服务渲染应用程序的新方法。使用它而不是 ReactDOM.hydrate与新的 React DOM 服务器 API 结合使用。没有它,React 18 中的新功能就无法工作

    createRoot 和 hydraRoot 都接受一个名为 onRecoverableError 的新选项,以便我们想在react从渲染 或者hydration期间的错误中恢复或日志记录时收到通知,默认情况下,在较旧的浏览器中,React使用reportError, 或console.error.

    # React DOM Server (opens new window)

    • 新API导出位置: react-dom/server,并且且完全支持在服务器上流式传输 Suspense
    1. renderToPipeableStream:用于node环境的流式传输

    2. renderToReadableStream:适用于现代边缘运行时环境,例如 Deno 和 Cloudflare worker

    # 06. 新的严格模式行为

    • 确保可重用状态:允许 React 在保留状态的同时添加和删除 UI 部分

    例如,当用户从一个屏幕上移开并返回时,React 应该能够立即显示上一个屏幕。为此,React 将使用与以前相同的组件状态卸载和重新安装树。 此功能将为 React 应用程序提供更好的开箱即用性能,但要求组件能够对多次安装和销毁的效果具有弹性。大多数效果无需任何更改即可工作,但有些效果假定它们只安装或销毁一次。

    为了帮助解决这些问题,React 18 为严格模式引入了一个新的仅限开发的检查。每当第一次安装组件时,此新检查将自动卸载并重新安装每个组件,并在第二次安装时恢复先前的状态。

    如果没有这个改变,当一个组件挂载时,React 会创建效果

    * React mounts the component.
      * Layout effects are created.
      * Effects are created.
    

    从 React 18 开始使用严格模式,每当组件在开发中安装时,React 将立即模拟卸载和重新安装

    * React mounts the component.
      * Layout effects are created.
      * Effects are created.
    * React simulates unmounting the component.
      * Layout effects are destroyed.
      * Effects are destroyed.
    * React simulates mounting the component with the previous state.
      * Layout effects are created.
      * Effects are created.
    

    在第二次挂载时,React 将从第一次挂载恢复状态。此功能模拟用户行为,例如用户从屏幕上移开并返回,确保代码能够正确处理状态恢复

    当组件卸载时,效果会正常销毁:

    * React unmounts the component.
      * Layout effects are destroyed.
      * Effect effects are destroyed.
    

    如何支持可重用性?

    举个例子:

    function ExampleComponent(props) {
      useEffect(() => {
        // Effect setup code...
    
        return () => {
          // Effect cleanup code...
        };
      }, []);
    
      useLayoutEffect(() => {
        // Layout effect setup code...
    
        return () => {
          // Layout effect cleanup code...
        };
      }, []);
    
      // Render stuff...
    }
    

    这个组件声明了一些在挂载和卸载运行的Effect。通常,这些Effect只会运行一次(在最初安装组件之后),而清理功能会运行一次(在卸载组件之后)。在React 18 Strict模式下,将发生以下情况:

    • React渲染组件
    • React挂载组件
    1. Layout effect代码运行
    2. Effect代码运行
    • React模拟隐藏或卸载的组件
    1. Layout effect清理代码运行
    2. Effect清理代码运行

    只要Effect在其自身之后进行清理(必要时返回清理方法),这通常不会导致问题。大多数Effect至少有一个依赖项。因此,它们可能已经能够适应多次运行,并且可能不需要任何更改。

    不过,仅在挂载上运行的Effect可能需要更改。从高层次来看,最有可能需要修改的影响类型分为两类:

    • 卸载时需要清理的Effect。
    • 只应运行一次的Effect(挂载时或依赖项更改时)。

    需要清理的效果应该具有对称性。

    无论是添加事件侦听器还是与某些命令式API交互,一般来说,如果Effect返回清理函数,那么它应该镜像设置函数。如今,许多组件使用下面所示模式的变体(variation)。

    }

    // A Ref (or Memo) is used to init and cache some imperative API.
    const ref = useRef(null);
      if (ref.current === null) {
      ref.current = new SomeImperativeThing();
    }
    
    // Note this could be useLayoutEffect too; same pattern.
    useEffect(() => {
      const someImperativeThing = ref.current;
      return () => {
        // And an unmount effect (or layout effect) is used to destroy it.
        someImperativeThing.destroy();
      };
    }, []);
    }
    

    如果上面的代码被卸载和重新装载,那么命令式的东西很可能会被破坏。(毕竟,它在第一次卸载后就被摧毁了。)为了解决这个问题,我们需要(重新)初始化mount上的命令。

    // 不使用Ref来初始化一些必需的东西!
    
    useEffect(() => {
      // 在同一个effect里面声明SomeImperativeThing,销毁它
      // 这样,如果组件被重新装载,它将被重新创建
      const someImperativeThing = new SomeImperativeThing();
    
      return () => {
        someImperativeThing.destroy();
      };
    }, []);
    

    有时,其他函数(如事件处理程序)也需要与命令进行交互。在这种情况下,可以使用ref来共享该值。

    // 使用Ref保存该值,但在Effect中对其进行初始化
    const ref = useRef(null);
    
    useEffect(() => {
      //   在同一个effect里面声明SomeImperativeThing,销毁它
      // 这样,如果组件被重新装载,它将被重新创建。
      const someImperativeThing = ref.current = new SomeImperativeThing();
    
      return () => {
        someImperativeThing.destroy();
      };
    }, []);
    
    const handeThing = (event) => {
      const someImperativeThing = ref.current;
      // 现在我们可以使用了
    };
    

    尽管不常见,但命令式API可能还需要与其他组件共享。在这种情况下,可以使用延迟初始化函数来暴露API。

    const ref = useRef(null);
    const getterRef = useRef(() => {
      if (ref.current === null) {
        ref.current = new SomeImperativeThing();
      }
      return ref.current;
    });
    
    useEffect(() => {
      return () => {
        if (ref.current !== null) {
          ref.current.destroy();
          ref.current = null;
        }
      };
    }, []);
    
    

    只能运行一次的Effect可以使用ref

    不需要清理的Effect——甚至挂载Effect——可能不需要任何更改就可以使用新的语义。

    useEffect(() => {
      SomeTrackingAPI.logImpression();
    }, []);
    
    

    这种效果是为了记录用户看到了特定内容。如果内容被隐藏,然后再次显示,该怎么办?它应该记录第二印象吗?(如果切换选项卡重新安装视图,这就是今天的效果。)如果可以的话,我们根本不需要改变效果。如果我们只想让效果记录一次,我们可以使用ref。

    在这种情况下,无论我们是挂载还是重新挂载,我们使用ref,这样我们只记录一次。

    # 07. 新Hooks

    # userId

    useId是一个新的钩子,用于在客户端和服务器上生成唯一的ID,同时避免不匹配。它主要用于与需要唯一ID的可访问性API集成的组件库

    提示

    useId不用于在列表中生成密钥。密钥应该由您的数据生成。

    # useTransition

    使用Transition和startTransition可以将某些状态更新标记为不紧急。默认情况下,其他状态更新被视为紧急状态。React将允许紧急状态更新(例如,更新文本输入)中断非紧急状态更新(例如,呈现搜索结果列表)

    • 将函数中的内容过渡,类比useCallback,将函数中的内容进行过渡
    • startTransition中触发的更新会让更高优先级(如外面的click)的更新先进行
    • startTransition中的延迟更新,不会触发Suspens组件的fallback,便于用户在更新期间的交互
    	// isPending: 过渡任务状态,true代表过渡中,false过渡结束
    	// startTransition: 执行的过渡任务
      const [isPending , startTransition] = useTransition()
    	startTransition(()=>{
    		// ...
    	})
    

    # useDeferredValue

    • 允许用户推迟屏幕更新优先级不高部分 如果说某些渲染比较消耗性能,比如存在实时计算和反馈,我们可以使用这个Hook降低其计算的优先级,使得避免整个应用变得卡顿
    import { useDeferredValue } from 'react';
    
    const deferredValue = useDeferredValue(value, { timeoutMs: <some value> });
    

    此命令设置值在timeoutMs中设置的时间后“滞后”。用户界面是必须立即更新还是必须等待数据,该命令使用户界面保持激活状态和响应性,该Hook避免了UI卡顿,并始终保持用户界面响应,以保持获取数据滞后的较小成本。

    • 过渡单个状态值,让状态滞后变化,类比useMemo, 对值进行过渡

    • 避开紧急任务的渲染,让出优先级

      • 如果当前渲染是一个紧急更新的结果,比如用户输入,React将返回之前的值,然后在紧急渲染完成后渲染新的值
      • React将在其他工作完成后立即进行更新(而不是等待任意的时间),并且像startTransition一样,延迟值可以挂起,而不会触发现有内容的意外回退。
    # useTransition/useDeferredValue 区别

    react 将更新分为两种,urgent update(紧急更新 )和transition update(过渡更新),useTransition/useDeferredValue都可以实现非紧急更新的延迟更新

    • 相同点:

      • useDeferredValue本质上和内部实现与useTransition一样都是标记成了过渡更新任务。
    • 不同点:

      • useDeferredValue在本质上就在Effect上执行的,而useEffect内部逻辑是异步执行的,所以它在一定程度上更滞后于useTransition

      • startTransition 的回调函数是同步执行的,会比 useDeferredValue 执行的时机更早

        • 在startTransition之中任何更新,都会标记上transition,React将在更新的时候,判断这个标记来决定是否完成此次更新。
        • 所以transition可以理解成比setTimeout更早的更新。
        • 但是同时要保证ui的正常响应,在性能好的设备上,transition两次更新的延迟会很小,但是在慢的设备上,延时会很大,但是不会影响UI的响应。

    # 与防抖的区别:

    • startTransition 和 useDeferredValue 与 setTimeout(防抖节流)的区别在于 不需要等待特定时间,可以监听任务的工作状态,当urgent update更新完毕,将会自动执行 transition update

    # 08. library hooks

    以下钩子是为库作者提供的,用于将库深入集成到React模型中,通常不在应用程序代码中使用。

    # useSyncExternalStore

    const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
    

    useSyncExternalStore是一个用于读取和订阅外部数据源的钩子,其方式与选择性hydration和时间切片等并发渲染功能兼容。

    此方法返回存储的值并接受三个参数:

    • subscribe:注册一个回调函数,每当存储发生更改时调用该函数
    • getSnapshot:返回存储的当前值函数。
    • getServerSnapshot:返回服务器渲染期间使用快照的函数。
    1. 最基本的例子就是订阅整个store
    const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
    
    1. 也可以订阅特定字段:
    const selectedField = useSyncExternalStore(
      store.subscribe,
      () => store.getSnapshot().selectedField,
    );
    
    1. 在服务器渲染时,必须序列化服务器上使用的存储值,并将其提供给useSyncExternalStore。React将在hydration过程中使用此快照,以防止服务器不匹配:
    const selectedField = useSyncExternalStore(
      store.subscribe,
      () => store.getSnapshot().selectedField,
      () => INITIAL_SERVER_SNAPSHOT.selectedField,
    );
    

    提示

    [getSnapshot]必须返回一个缓存值。如果连续多次调用 getSnapshot,则它必须返回相同的确切值,除非两者之间有存储更新。 提供了一个shim,用于支持作为use-sync-external-store/shim发布的多个版本.此 shim 将useSyncExternalStore在可用时优先使用, 不可用时回退到user-space 实现

    # useInsertionEffect

    useInsertionEffect(didUpdate);
    

    签名与useEffect相同,但它在所有DOM mutations(React执行对DOM的更新阶段)之前同步激发。在useLayoutEffect中读取布局之前,使用此选项将样式注入DOM。因为这个钩子的作用域有限,所以这个钩子不能访问refs,也不能调度更新。

    提示

    seInsertionEffect应仅限于 css-in-js 库作者。首选useEffect或useLayoutEffect。

    #react
    react 事件机制
    react 架构演变

    ← react 事件机制 react 架构演变→

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式