咱们来自字节跳动飞书商业运用研发部(Lark Business Applications),目前咱们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。咱们重视的产品范畴主要在企业经历办理软件上,包含飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 范畴系统,也包含飞书审批、OA、法务、财政、收购、差旅与报销等系统。欢迎各位加入咱们。

本文作者:飞书商业运用研发部 黄兆龙

欢迎咱们重视飞书技能,每周定时更新飞书技能团队技能干货内容,想看什么内容,欢迎咱们谈论区留言~

React Query 原理与规划

前语

实践事务开发中,除了组件开发、状况办理,数据恳求也是一个比较重要的部分。

React Query 是一个以 hook 的方法办理恳求的恳求办理库,目前在 Github 上有 30k star。

它的功用十分强壮,包含轮询,重试,缓存,SWR 等高级能力,但 API 却十分简略。

本文不介绍 React Query 的各种 API 和用法, 只从一个最根底的比方,认识它的原理和思维,以进步代码规划能力。

最根底的比方

比方,要在组件挂载时获取展现一组 todos 数据,而且在恳求发送时展现 loading 态。

不运用 React Query,普遍的写法是运用 useState 声明状况,运用 useEffect 发送恳求。

const fetchTodos = () => {
  return axios.get('/api/todos/');
}
function Example() {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState([]);
  useEffect(() => {
    fetchTodos().then((res) => {
      setData(res.data);
      setIsLoading(false);
    });
  }, []);
  if (loading) ...
  ...
}

再来看看运用了 React Query,是怎么声明恳求的。

首先在 React 组件顶层 App 外部实例一个恳求客户端 queryClient,它会办理默认装备和大局状况,并注入。

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient();
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

接着在组件里运用 useQuery 改写上面的 useState 和 useEffect,它会主动办理恳求的 loading , data , error 等状况。

import { useQuery } from '@tanstack/react-query'
function Example() {
  const { data, isLoading } = useQuery('todos', fetchTodos);
  if (isLoading) ...
  ...
}

useQuery 以声明式的方法界说恳求,减少了许多模板代码。总共接纳 3 个参数:

useQuery(queryKey, queryFunction, options);
  • queryKey:恳求唯一标识
  • queryFunction:返回 thenable 的函数
  • options:恳求相关装备

模板代码都到哪了

已然代码里不必 useState 声明状况和发送恳求。React Query 是不是把这些模板代码都搬到 useQuery 内部里去界说了呢?

试想这样一个场景,假定页面上有三个子组件 A,B,C(下方的蓝色方块),都需要用到同一个接口的 todo 数据。

React Query 原理与规划

如果仅仅把模板代码搬到 useQuery 内部的话,3 次 useQuery 就会发送 3 个相同的恳求。

// compA
const { data, isLoading } = useQuery('todos', fetchTodos);
// compB
const { data, isLoading } = useQuery('todos', fetchTodos);
// compC
const { data, isLoading } = useQuery('todos', fetchTodos);

React Query 原理与规划

但是实践上,恳求只会宣布一次

原因便是 3 个 useQuery,咱们都界说了同一个值为 'todos' 的 queryKey,它代表恳求唯一标识

React Query 的本质

在 Reacr Query 中,代表恳求唯一标识的并不是恳求 path,而是 queryKey,它作为 useQuery 必传的第一个参数,接纳字符串,数组,对象等一切能够被序列化的值。

上面声明的 queryKey 都为 'todos',接纳到 queryKey 后,useQuery 会在内部找到或者创立与之对应的 Query 实例,Query 实例包含 isLoading,data 等状况,queryKey 与 Query 实例一一对应。

React Query 原理与规划

所以恳求也都收敛到了 Query 实例内部宣布,直接与服务端交互。

3 个 queryKey 对应 1 个 Query 实例,所以只会有一次恳求。

React Query 原理与规划

那么 Query 实例保存在哪个当地呢?在 React 组件内部吗?

还记得最开始咱们在 App 中做过什么吗,咱们有初始化过 QueryClient,并把它注入到了整个运用中。

import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App () {
  return (
   <QueryClientProvider client={queryClient}>
     <Example />
   </QueryClientProvider>
  )
}

它会在内部将 Query 办理起来,能够将 queryClient 看作一个外部的 store。Query 以 map 键值对的方法保存在 store 中,key 为 queryHash(也便是 queryKey 序列化往后的值),value 为 Query。

this.queriesMap = {};
this.queriesMap[query.queryHash] = query;

在图上加上 queryClient,全体流程如下,它的本质便是一个外部的状况办理库。

React Query 原理与规划

这又引出了一个新的问题:

已然 Query 保存在组件外部,恳求的 data,loading 等状况也都脱离了 React 办理,那么 React 是怎么感知到它们的改变,来影响渲染的呢?

观察者规划形式

怎么观测数据,观测到数据变更后怎么更新。 这其实是一切状况办理库的中心命题。

React Query 的完成运用了观察者规划形式,这也是它的一个中心。

上面的比方中,App 内有多个当地都依靠了 queryClient 中的同一个 todos 数据。有了观察者,就能够在这部分数据更新时,告诉到每一个订阅者。

当每次调用 useQuery 时,内部都会实例一个观察者对象 observer。

const [observer] = React.useState(
    () =>
      new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
        queryClient,
        defaultedOptions,
      ),
  )

observer 会订阅 query 的状况,当这些状况改变时,触发 React 强制更新。

在 React 中,手动强制触发更新无非便是 forceUpdate。React Query 在 v3 版别便是这么做的:

const [, forceUpdate] = React.useState(0);
React.useEffect(() => {
    // ...
    // 省掉了部分代码,只保留了订阅的部分
    const unsubscribe = observer.subscribe(
      notifyManager.batchCalls(() => {
        if (mountedRef.current) {
          forceUpdate(x => x + 1)
        }
      })
    )
    return () => {
      unsubscribe()
    }
  }, [...])

传入 notifyManager.batchCalls 的便是触发更新的方法。

但是在 React 18 发布后,因为 concurrent 并发特性的原因,forceUpdate 或许会造成撕裂问题, 已经不能再持续运用了。

撕裂问题

什么是撕裂问题?这里引证【3Shain的答复 – 知乎】简略描绘一下:

浏览器 JS 是单线程的,当控制权交给 js 代码时,直到代码执行同步结束之前浏览器都是堵塞的。所以如果代码执行时间过长,用户就会感知到显着的不流畅。

解决办法有两个方向:

  • 一是减少计算量
  • 二是将计算使命分红多个小块,每执行一小块后就把控制权还给浏览器,浏览器在某个时间再把控制权再还回来。

在之前的 react 版别中,render 进程是不能被中断的,而 react 18 引入的 fiber 规划就使得 render 这个进程能够切分红以 fiber 为最小单位的屡次使命。

这里就引出了问题的根源:一次 render 或许不是全体同步执行的,中心还或许穿插着其他的非 render 使命被执行,其间就有或许包含了对外部状况的修正。

React Query 原理与规划

假定咱们的一次 render 被分红了两部分,两部分都读取了外部状况 A。如果两部分使命之间,浏览器处理了一个事情使得外部状况 A 发生了改变,那前半部分的使命读取到的是旧值,后半部分读取到的却是新值,这就造成了渲染结果的不一致性,这便是撕裂问题,也叫 tearing。

github.com/reactwg/rea…

useSyncExternalStore

为了避免 tearing 的出现,React 18 给出了官方答案,推出了新的 API:useSyncExternalStore, 直译为 “同步外部 store”。

用来从外部数据源读取和订阅状况,而且与并发特性兼容。是 Recat 专门供给给第三方库的 hook,通常不必于常规的 App 开发。

它的原理便是在 tearing 出现时,告诉 React 之前读取过的外部状况发生了改变,让 React 重新触发一次同步更新,以保证最终结果一致性。

它的用法如下,基于外部的 store 创立一个 state,接纳 3 个参数:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
  • subscribe:订阅函数 ,react 会给订阅函数传入一个 onStoreChange 函数,当外部 store 改变时,有必要调用 onStoreChange 告诉到 React
  • getSnapshot:要订阅的状况
  • getServerSnapshot:在 SSR 时要订阅的状况,可选。

所以,React Query 在最新的 v4 将 forceUpdate 换成了 useSyncExternalStore

useSyncExternalStore(
    React.useCallback(
      (onStoreChange) =>
        isRestoring
          ? () => undefined
          : observer.subscribe(notifyManager.batchCalls(onStoreChange)),
      [observer, isRestoring],
    ),
    () => observer.getCurrentResult(),
    () => observer.getCurrentResult(),
  )

能够看到,传入notifyManager.batchCalls 触发更新方法从 forceUpdate 换成了 react 供给在 API 里供给的 onStoreChange

现在 Query 的改变能够触发 React 更新了,省掉了其它 Query,最终全体流程如下:

React Query 原理与规划

与结构无关

简略总结一下 React Query 的流程:

  1. 与恳求相关的底层逻辑都封装在了 Query 中,直接与服务端交互
  1. 同时 Query 又被保管在外部 store 的 queryClient 中
  1. queryClient 会在 App 顶层运用 Provider 大局注入到 React
  1. 组件运用 hook 与 Query 建立衔接,订阅状况触发更新

能够发现,1,2 点是恳求 Query 的中心逻辑,它是与结构无关的。3,4 点是与 React 结构结合,建立通讯的部分。

所以作者在安排代码的时分,有意将这两部分代码进行了拆分,从架构上完成了「 数据获取 」这个范畴逻辑与 React 结构逻辑的别离。

这两部分在源码文件目录里别离命名为 query-corereact-query,拆分红了两个文件夹独自办理,并在 v4 版别完成独自发布。库房也从 react-query 更名为 query。

packages/
├── query-core/
├── react-query/
├── ...

作为与结构无关的 query-core 部分,它能够与 React 结合出 react-query。

那么它也肯定能够和其它结构结合,诞生出比方 vue-query,solid-query。从官网上也能够看出这也是目前作者正在做的事。

React Query 原理与规划

除了完成范畴逻辑的别离,React Query 文件夹内部的文件安排也十分值得学习。

来看看 query-core 和 react-query 这两个文件夹。

query-core/
├── src/
│     ├── query.ts
│     ├── queryCache.ts
│     ├── queryClient.ts
│     ├── queryObserver.ts
│     ...
...
react-query/
├── src/
│     ├── QueryClientProvider.tsx
│     ├── useMutation.ts
│     ├── useQueries.ts
│     ├── useQuery.ts
│     ...
...

文件都以 API 称号命名,十分清晰,一目了然。即使是第一次阅览源码的人也能直观地理解到这些文件的功用大概。

总结

以上便是本文的一切内容,总结一下:

  • React Query 本质是一个外部的状况办理库, 它的中心逻辑与 React 结构无关。
  • 在处理与结构衔接部分,运用了观察者规划形式来处理恳求状况的订阅和更新。
  • 因为中心逻辑与结构不耦合,使得它也能与 Vue,Solid 这些结构结合。
  1. 经过深入源码学习剖析它的原理与规划,能有效提高咱们的代码逻辑思维。

加入咱们

扫码发现职位&投递简历

React Query 原理与规划

官网投递:job.toutiao.com/s/FyL7DRg