从一个小 DEMO 认识 useTransition、useDeferredValue

引言

React18 中最引人瞩目的要数 Concurrent Mode(并发模式), 它并不是一个具体功能, 而是一个底层设计!! 在 Concurrent Mode 下更新的 reconcile(调度)过程被认为是可中断的, 这就使得浏览器渲染进程不会被一个耗时较长的 reconcile(调度) 阻塞而导致页面在交互过程中出现卡顿, 同时将任务按照优先级进行了划分, 让更高优先级的更新优先被处理!!

对于 React 18 可通过新的 Render API 来启用 Concurrent Mode(并发模式)

js 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);

但是呢? 如果真正要触发 并发更新 还需要配合 useTransition 或者 useDeferredValue 进行使用, 而这两个 API 的使用也正是我们今天要讲的主角!!!

一、从一个 DEMO 开始

在开始前, 我们先看一个例子, 如下代码所示:

  1. 输入框 Input 输入词条将调用 handleChange 修改状态 keyword 的值
  2. Card 中渲染了 300 条数据, 每条数据在渲染时手动延迟 1ms, 展示内容为 输入词条: {keyword}, 当然这里进行了组件的拆分, 同时使用了 memo 来减少一些不必要的渲染
js 复制代码
const Item = ({ keyword }) => {
  // 增加 1ms 渲染时间s
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {}

  return <div>输入词条: {keyword}</div>;
};

const List = memo(({ keyword }) => {
  return Array(300)
    .fill("a")
    .map((v, index) => <Item key={index} keyword={keyword} />);
});

export default function App() {
  const [keyword, setKeyword] = useState(null);

  const handleChange = useCallback((e) => {
    setKeyword(e.target.value);
  }, []);

  return (
    <div className="App">
      <Card
        bordered={false}
        title={<Input placeholder="输入词条" onChange={handleChange} />}
      >
        <List keyword={keyword} />
      </Card>
    </div>
  );
}

上面 👆🏻 代码运行结果可想而知, 我们如果在输入框中输入数据, 会很明显感觉到卡顿

而这里造成卡顿的原因其实也简单: 由于浏览器是单线程的, 在我们输入词后 React 列表会进行重新渲染(花费较长时间), 导致输入框无法及时响应我们的交互, 从而给用户造成了页面卡顿的现象

二、useTransition 改造

2.1 简介

  1. useTransition 是一个 React Hook, 可让我们实现在不阻塞 UI 的情况下更新状态
js 复制代码
import { useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  // ...
}
  1. 如上代码, 调用该 Hook:
  • 不接受任何参数
  • 返回一个包含两个元素的数组, 第一个字段(isPending)表示是否等待中, 第二参数(startTransition) 接收一个函数参数, 函数中修改状态的任务将会被标记为低优先级(不紧急)任务, 同时在修改状态执行任务其中随时会被中断停止, 执行高优先级的任务, 从而避免阻塞用户交互、UI 渲染等高优先级任务
js 复制代码
import { useTransition, useCallback, useState } from 'react';

function TabContainer() {
  const [num, setNum] = useState(0)
  const [isPending, startTransition] = useTransition();

  const handleChange = useCallback((v) => {
    startTransition(() => {
      // 该任务被标记为低优先级任务, 不阻塞用户交互、`UI` 渲染、高优先级任务
      setNum(v)
    })
  }, [])
  // ...
}
  1. 注意事项:
  • useTransition 是一个 Hook, 所以只能在函数组件内部或者自定义 Hooks 中调用, 如果需要在其他地方(例如类组件)可以使用独立模式, React 提供了一个专门的 api startTransition 来实现
  • 传递给 startTransition 的函数必须是同步的
  • startTransition 中被标记的 状态更新 任务, 会被其他 高优先级 任务中断

2.2 改造

如下代码, 我们使用 useTransition 进行了简单的改造, handleChange 中使用 startTransition 并在其回调函数中调用 setKeyword 修改状态, 如此修改状态也就是 setKeyword 的任务将会被标记为非紧急, 会优先响应用户的交互, 从而及时响应用户的操作

diff 复制代码
export default function App() {
+ const [isPending, startTransition] = useTransition();
  const [keyword, setKeyword] = useState(null);

+ const handleChange = useCallback(
+   (e) => {
+     startTransition(() => setKeyword(e.target.value));
+   },
+   [startTransition]
+ );

  return (
    <div className="App">
      <Card
        bordered={false}
        title={<Input placeholder="输入词条" onChange={handleChange} />}
      >
        <List keyword={keyword} />
      </Card>
    </div>
  );
}

最后的效果如下, 在我们快速输入词条的情况下, 输入框会及时响应用户, 下面的展示区域则会延迟进行展示

下面我们可以借用 isPending 为展示区域设置一个 pending 效果, 修改代码如下:

diff 复制代码
export default function App() {
  const [isPending, startTransition] = useTransition();
  const [keyword, setKeyword] = useState(null);

  const handleChange = useCallback(
    (e) => {
      startTransition(() => setKeyword(e.target.value));
    },
    [startTransition]
  );

  return (
    <div className="App">
      <Card
        bordered={false}
        title={<Input placeholder="输入词条" onChange={handleChange} />}
      >
+       <Spin spinning={isPending}>
+         <List keyword={keyword} />
+       </Spin>
      </Card>
    </div>
  );
}

最后效果如下, 当我们快速输入词条, 输入框能够正常反馈, 展示区域将延迟展示, 同时还会显示加载状态

三、 useDeferredValue 改造

3.1 简介

  1. useDeferredValue 是一个 React Hook, 可以让某个值(可以是 props 也可以是 state)延迟更新, 从而推迟部分 UI 的更新
js 复制代码
import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}
  1. 如上代码, 该 hooks 使用起来其实也很简单:
  • 接收一个参数, 也就是需要延迟生效的状态或者 props
  • 返回一个值, 可以理解为是新的状态, 该状态是会 延迟更新

3.2 改造

如下代码, 使用 useDeferredValue 基于 keyword 生成新的值 deferredKeyword, 只是 deferredKeyword 值的更新任务会被标记为低优先级, 从而避免阻塞用户的交互

diff 复制代码
export default function App() {
  const [keyword, setKeyword] = useState(null);

+ // 根据 keyword 延迟更新
+ const deferredKeyword = useDeferredValue(keyword);

  const handleChange = useCallback((e) => {
    setKeyword(e.target.value);
  }, []);

  return (
    <div className="App">
      <Card
        bordered={false}
        title={<Input placeholder="输入词条" onChange={handleChange} />}
      >
+       <List keyword={deferredKeyword} />
      </Card>
    </div>
  );
}

最终效果如下, 输入框快速输入, 页面能够正常响应, 展示列表延迟在空闲时才会进行更新(延迟更新)

这里我们可以尝试在 useEffect 中打印下 keyworddeferredKeyword

diff 复制代码
export default function App() {
  const [keyword, setKeyword] = useState(null);

  // 根据 keyword 延迟更新
  const deferredKeyword = useDeferredValue(keyword);

  const handleChange = useCallback((e) => {
    setKeyword(e.target.value);
  }, []);

+ useEffect(() => {
+   console.log("keyword: ", keyword);
+   console.log("deferredKeyword: ", deferredKeyword);
+ }, [keyword, deferredKeyword]);

  return (
    <div className="App">
      <Card
        bordered={false}
        title={<Input placeholder="输入词条" onChange={handleChange} />}
      >
        <List keyword={deferredKeyword} />
      </Card>
    </div>
  );
}

从打印结果来看, 在快速输入情况下 deferredKeyword 一直为空, 只有在停止输入(空闲)时才会进行更新

3.3 useDeferredValue 实现

设想下, 如果让你实现 useDeferredValue 你会怎么做呢? 从它的特性也就是「依赖某个值」「延迟更新」「新的状态」 这几点出发, 其实我们也能猜到大概的一个实现代码:

  • 新建一个状态值 newValue
  • 使用 useTransition 来延迟更新状态值 newValue
  • 使用 useEffect 监听依赖值 value 的变更, 并调用 startTransition 来延迟更新新状态
js 复制代码
const useDeferredValue = (value) => {
  const [newValue, setNewValue] = useState(value)
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    startTransition(() => {
      setNewValue(value)
    })
  }, [value, startTransition])

  return newValue
} 

四、和防抖、节流的区别

  1. 防抖: 等待一段时间(例如一秒钟), 用户没有任何操作(触发事件), 则执行事件处理函数

  2. 节流: 指定时间段内(例如一秒钟), 用户多次操作(触发事件), 只会执行第一次事件处理函数

  3. 而本文提到的不管是 useDeferredValue 还是 useTransition 都是借用并发更新特性, 将任务标记为低优先级, 将主线程让给其他更高优先级任务, 从而保障用户的一个交互体验!!

  • 与防抖或节流不同, 它不需要选择任何固定延迟!!! 如果用户的设备速度很快(例如功能强大的笔记本电脑), 则延迟几乎会立即发生并且不会被注意到。如果用户的设备速度很慢, 则延迟较为明显。
  • 此外, 与防抖或节流不同, useDeferredValue 或者 useTransition 默认情况下是可随时中断的, 这意味着如果 React 正在重新渲染一个大列表, 但用户再次进行击键, React 将放弃本次低优先级任务, 优先响应用户操作, 然后再继续执行低优先级任务。同样情况再防抖和节流仍然会产生卡顿, 因为它们执行期间是无法被停止的。

五、参考

相关推荐
一生为追梦1 小时前
Linux 内存管理机制概述
前端·chrome
喝旺仔la1 小时前
使用vue创建项目
前端·javascript·vue.js
心.c1 小时前
植物大战僵尸【源代码分享+核心思路讲解】
前端·javascript·css·数据结构·游戏·html
喝旺仔la1 小时前
Element Plus中button按钮相关大全
前端·javascript·vue.js
柒@宝儿姐2 小时前
Git的下载与安装
前端·javascript·vue.js·git·elementui·visual studio
Hiweir ·2 小时前
机器翻译之数据处理
前端·人工智能·python·rnn·自然语言处理·nlp·机器翻译
曈欣3 小时前
vue 中属性值上变量和字符串怎么拼接
前端·javascript·vue.js
QGC二次开发3 小时前
Vue3:v-model实现组件通信
前端·javascript·vue.js·前端框架·vue·html
努力的小雨4 小时前
从设计到代码:探索高效的前端开发工具与实践
前端
小鼠米奇5 小时前
详解Ajax与axios的区别
前端·javascript·ajax