🌑

Mocha's Blog

目录
  1. 重中之重 - use hook
  2. 涅槃重生 - Suspense
    1. 工作原理
  3. 使用技巧
    1. 按需使用 use
    2. 多次触发 promise
  4. 忘掉 memo
    1. 原理
  5. 其他
    1. 移除 forwardRef
    2. 去掉 Context.Provider
  6. 总结
  7. OneMoreThing

React 19 - 异步编程新范式

发布时间:2024年7月31日

在 2024.5.15 -16 日举行 ReactConf 2024 大会上 React 团队正式对外发布了 React 19 的第一个 beta 版本。
React 19 包含了重磅新特性,如:

  • 新的 use hook ,为 react 中的异步编程场景提供了新方案,也帮助 Suspense 可以上桌吃饭了
  • ReactConf 2021 上画的大饼 —— react forget 终于在 ReactConf 2024 上正式推出,被寄希望解决长期困扰 React 的性能问题

回顾过去几年,自 React 16.8 以后,React 先后推出一些重要的功能:

  1. react hook 的推出将函数式组件从食之无味的境地中解放出来,赋予其与类组件相同的地位,带来函数式编程在前端开发中的大规模落地,并影响到现在的前端生态
  2. 使用 Fiber 架构 & Reconciler Diff ,实现了渲染的可打断性,迈出了解决性能问题的第一步
  3. 一些新的 API,比如:
  • 新的 createRoot API,重构事件的挂载机制,解决多版本多实例下可能的事件触发问题 - react@17
  • 新的 useTransition 提供了原生的类防抖函数,useDeferredValue 在防抖的基础上还可以叠加数据派生功能 - react@18

上述有些 API 在我们日常的开发中并不常用,日常更多的还是通过基础的 hook 去实现业务逻辑,实际上在即将到来的 react@19 中,我们可以再一次升级我们的开发范式

重中之重 - use hook

在 React@19 中,官方新推出了一个名为 use 的 hook,一个没有任何场景语义的 hook,却能代表 React@19 的趋势 —— 重构异步编程范式。
先看一下使用方法:

// api.ts
export const sayHello = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: '影和' });
    }, 2000);
  });
};
// app.tsx
import { Suspense, use } from 'react';
import { sayHello } from './api';

function Message() {
  const content = use(sayHello());

  return <h1>{content.name}</h1>;
}

function App() {
  return (
    <Suspense fallback={<span>loading...</span>}>
      <Message />
    </Suspense>
  );
}

export default App;

通过 Suspense + use hook 的结合使用,我们终于可以在代码中像同步代码一样去编写异步逻辑,在轻量化的场景下完全可以摆脱对 useRequest / useQuery 等三方请求库的依赖
在使用 use 时有几个点需要注意:

  1. use 的内容必须是一个 Promise,use 的结果是其 resolve 的值
  2. 不同于其他的 hook,use 可以放在循环或者条件语句中使用
  3. use 必须和 Suspense 搭配一起使用,且使用 use 的组件必须为 Suspense 的子组件

涅槃重生 - Suspense

在 React 19 中,use(promise) 被设计成完全符合 Suspense 规范的 hook,我们可以将使用了 use(promise)的函数式组件看做成一个异步组件,即 —— Promise<React.FC>。
在这种视角下,Suspense 就相当于我们在 async / await 中使用的 try / catch,也就是说 Suspense 其实扮演了 ErrorBoundry 的角色,Suspense 中的 fallback 对应到 catch 的逻辑。

工作原理

在 React 源码中,Suspense 的子组件被称为 primary,在 React 的 diff 流程中遇到 Suspense 组件时,首先会尝试加载 primary 组件:

  • 如果 primary 组件只是一个普通组件,那么在下一次渲染时将直接渲染
  • 如果 primary 组件是一个包含了 use 读取异步 promise 的组件
    • 在首次渲染时,先抛出一个异常
    • react 捕获到该异常之后,发现这个异常是我们使用 use 故意产生的,那么就会将其 then 的回调函数保存下来,并回滚指针重新指向 Suspense
    • 此时 promise 在请求阶段,Suspense 会直接渲染 fallback
    • primary 中的 promise 执行完成时「resolve」,会执行刚才保存的 then 方法,此时会触发 Suspense 的更新。由于此时 primary 中的 promise 已经 resolve,因此此时就可以拿到数据直接渲染 primary 组件。

整个流程可以表示为:

image.png

使用技巧

按需使用 use

相较于传统的的 useState / useEffect 等 hook ,use 有一个最大的特点 —— 它可以按需使用,即你的组件可以是这样的

function Message() {
  const [flag, setFlag] = useState<boolean>();

  let content = { name: 'default' };

  if (flag) {
    content = use(sayHello());
  }

  return (
    <>
      <h1>{content.name}</h1>
      <button
        onClick={() => {
          setFlag(true);
        }}
      >
        set flag
      </button>
    </>
  );
}

我们可以将这种特性应用在某一个同时支持创建/编辑功能表单的页面中,用初始化的值作为表单的 defaultValues,如果是在编辑页面就可以发出请求查询详情回显

import React, { use } from '@alipay/bigfish/react';
import { Form } from '@alipay/bigfish/antd';
import { useParams } from '@alipay/bigfish';

export default () => {
  const { userId } = useParams();

  const [form] = Form.useForm();

  let initialValues = { name: 'default' };

  if (userId) {
    initialValues = use(queryUserDetail({ userId }));
  }

  return (
    <Form form={form} initialValues={initialValues}>
      <Form.Item name='name'>
        <Input />
      </Form.Item>
      <Form.Item name='password'>
        <Input.Password />
      </Form.Item>
    </Form>
  );
};

多次触发 promise

在组件里,如果只是常规的使用 use(promise) 发出请求,该 hook 在组件的声明周期中只会执行一次,但有时候在某些场景下(如页面详情,手动触发刷新),我们需要多次触发刷新,此时我们可以借助 useState 来实现 use(promise) 的多次触发。
在使用中,我们可以将 use(promise) 视为一个 dependency 为传入的 promise 的 useEffect,每当 promise 变动时,都会重新触发请求

function Message() {
  const [promise, update] = useState<Promise<any>>(sayHello());

  const content = use(promise);

  return (
    <>
      <h1>{content.name}</h1>
      <button onClick={() => update(sayHello())} style={{ marginTop: 16 }}>
        Refresh
      </button>
    </>
  );
}

忘掉 memo

在实际开发中,我们经常遇到对多个数据进行派生的场景,大多数情况下大家都会用 useMemo 去解决,因为其足够简单高效且符合直觉。实际上,在 React@18 中,官方提供了一个新的 hook —— useDeferredValue,似乎更适合这种场景。
useDeferredValue 的名字以及目前大多数文章中对其的介绍来看,其最佳的用法是在解决 state 短时间内高频词刷新导致的性能问题上,比如:我们有一个大数量列表下前端过滤的功能,如果实时做 filter,可能会出现一些性能问题。

// 1-1000
const fullList = new Array(1000)
  .fill(0)
  .map((_, index) => Date.now() + `_${index}`);

function Message() {
  const [keyword, setKeyword] = useState<string>();

  const showList = fullList.filter((item) => {
    if (!keyword) {
      return true;
    }
    return item.includes(keyword);
  });

  return (
    <>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        style={{ border: '1px solid #000' }}
      />
      <ul>
        {showList?.map((name) => {
          return <li key={name}>{name}</li>;
        })}
      </ul>
    </>
  );
}

仅仅是 1000 条数据,在实时做过滤时就可能出现性能上的瓶颈(搜索框卡顿),此时我们可以使用 useDeferredValue 优化我们的代码,将 filter 逻辑改为如下:

const showList = useDeferredValue(
  fullList.filter((item) => {
    if (!keyword) {
      return true;
    }
    return item.includes(keyword);
  })
);

相较于直接的 filter 逻辑,用户在输入框的层面的交互已经顺畅很多。
相较于其他文章中介绍的 useDeferredValue 用法,我们应该认识到这个 hook 的关键在于 deferred,而不是你传给它的是 originValue 还是 derivedValue。

原理

在 React Reconciler 中会将任务分为多个优先级,当我们对一个数据使用 useDeferredValue 进行包裹时,相当于手动将其声明为了低优先级的任务,React 会在完成其他高优先级任务时再回来计算该数据,从而引起对应的视图更新,React 会自动管理触发计算的时间(requestAnimationFrame & requestHandleCallback)。
这样处理有一个好处在于,如果用户设备性能很强悍,他不会感觉到有任何明显的差异,如果设备性能很差,那么这套调度的优势就会显现出来。

其他

移除 forwardRef

在 React@19 中,ref 的传递不再依赖于 forwardRef API,可以将 ref 通过 过props 的方式直接传递给子组件。

// old
export default React.forwardRef(function App(props, ref) {
  // 组件逻辑
})

// new
export default function App({ ref, ...props }) {
  // 组件逻辑
}

去掉 Context.Provider

在 React@19 中,通过 useContext 创建的 context 可以直接通过 的形式去包裹子组件,而无需再去使用 <Context.Provider></Context.Provider> 这类格式了

const MyContext = React.useContext({});

// old
export default () => {
  <MyContext.Provider>
    <App />
  </MyContext.Provider>
}

// new
export default () => {
  <MyContext>
    <App />
  </MyContext>
}

总结

以上内容是我们可以直观看到的 React@19 带来的新的特性,简单来说 React@19 为我们带来异步编程的新范式。
可能大家觉得这些东西并不如 ahooks、react-query 等好用,而且这几年 React 推出的很多新的 API 也并没有特别为大家所知,但实际上这些东西已经在前端库中大量的运用,只是我们对这些东西的感知并屏蔽掉了。
除了上面提到的这些东西,还有一些如:useTransition、useActionState、useOptimistic,这些在业务层面可能接触不多的东西就不再多写,大家可以自行了解。

OneMoreThing

在 React@19 中,其实隐藏了一个最最重磅的更新 —— 鸽了三年的 React Compiler(Forgot)。
有了 Compiler 加持,在未来我们可以抛弃所有的 memo / useMemo / useCallback / useDeferredValue,一个完全符合直觉、无需性能优化的函数式编程未来即将到来。

Powered By Hexo.js Hexo and Minima. Support By Oracle & Docker-Compose.