zoukankan      html  css  js  c++  java
  • 精读Hooks 取数-swr源码

    1 引言

    取数是前端业务的重要部分,也经历过几次演化:

    • fetch 的兼容性已经足够好,足以替换包括 $.post 在内的各种取数封装。
    • 原生用得久了,发现拓展性更好、支持 ssr 的同构取数方案也挺好,比如 isomorphic-fetch、axios。
    • 对于数据驱动场景还是不够,数据流逐渐将取数封装起来,同时针对数据驱动状态变化管理进行了 data isLoadingerror 封装。
    • Hooks 的出现让组件更 reactive,我们发现取数还是优雅回到了组件里,swr 就是一个教科书般的例子。

    swr 在 2019.10.29 号提交,仅仅 12 天就攒了 4000+ star,平均一天收获 300+ star!本周精读就来剖析这个库的功能与源码,了解这个 react Hooks 的取数库的 Why How 与 What。

    2 概述

    首先介绍 swr 的功能。

    为了和官方文档有所区别,笔者以探索式思路介绍这个它,但例子都取自官方文档。

    2.1 为什么用 Hooks 取数

    首先回答一个根本问题:为什么用 Hooks 替代 fetch 或数据流取数?

    因为 Hooks 可以触达 UI 生命周期,取数本质上是 UI 展示或交互的一个环节。 用 Hooks 取数的形式如下:

    import useSWR from "swr";
    
    function Profile() {
      const { data, error } = useSWR("/api/user", fetcher);
    
      if (error) return <div>failed to load</div>;
      if (!data) return <div>loading...</div>;
      return <div>hello {data.name}!</div>;
    }

    首先看到的是,以同步写法描述了异步逻辑,这是因为渲染被执行了两次。

    useSWR 接收三个参数,第一个参数是取数 key,这个 key 会作为第二个参数 fetcher 的第一个参数传入,普通场景下为 URL,第三个参数是配置项。

    Hooks 的威力还不仅如此,上面短短几行代码还自带如下特性:

    1. 可自动刷新。
    2. 组件被销毁再渲染时优先启用本地缓存。
    3. 在列表页中浏览器回退可以自动记忆滚动条位置。
    4. tabs 切换时,被 focus 的 tab 会重新取数。

    当然,自动刷新或重新取数也不一定是我们想要的,swr 允许自定义配置。

    2.2 配置

    上面提到,useSWR 还有第三个参数作为配置项。

    独立配置

    通过第三个参数为每个 useSWR 独立配置:

    useSWR("/api/user", fetcher, { revalidateOnFocus: false });

    配置项可以参考 文档。

    可以配置的有:suspense 模式、focus 重新取数、重新取数间隔/是否开启、失败是否重新取数、timeout、取数成功/失败/重试时的回调函数等等。

    第二个参数如果是 object 类型,则效果为配置项,第二个 fetcher 只是为了方便才提供的,在 object 配置项里也可以配置 fetcher。

    全局配置

    SWRConfig 可以批量修改配置:

    import useSWR, { SWRConfig } from "swr";
    
    function Dashboard() {
      const { data: events } = useSWR("/api/events");
      // ...
    }
    
    function App() {
      return (
        <SWRConfig value={{ refreshInterval: 3000 }}>
          <Dashboard />
        </SWRConfig>
      );
    }

    独立配置优先级高于全局配置,在精读部分会介绍实现方式。

    最重量级的配置项是 fetcher,它决定了取数方式。

    2.3 自定义取数方式

    自定义取数逻辑其实分几种抽象粒度,比如自定义取数 url,或自定义整个取数函数,而 swr 采取了相对中间粒度的自定义 fetcher:

    import fetch from "unfetch";
    
    const fetcher = url => fetch(url).then(r => r.json());
    
    function App() {
      const { data } = useSWR("/api/data", fetcher);
      // ...
    }

    所以 fetcher 本身就是一个拓展点,我们不仅能自定义取数函数,自定义业务处理逻辑,甚至可以自定义取数协议:

    import { request } from "graphql-request";
    
    const API = "https://api.graph.cool/simple/v1/movies";
    const fetcher = query => request(API, query);
    
    function App() {
      const { data, error } = useSWR(
        `{
          Movie(title: "Inception") {
            releaseDate
            actors {
              name
            }
          }
        }`,
        fetcher
      );
      // ...
    }

    这里回应了第一个参数称为取数 Key 的原因,在 graphql 下它则是一段语法描述。

    到这里,我们可以自定义取数函数,但却无法控制何时取数,因为 Hooks 写法使取数时机与渲染时机结合在一起。swr 的条件取数机制可以解决这个问题。

    2.4 条件取数

    所谓条件取数,即 useSWR 第一个参数为 null 时则会终止取数,我们可以用三元运算符或函数作为第一个参数,使这个条件动态化:

    // conditionally fetch
    const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
    
    // ...or return a falsy value
    const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);

    上例中,当 shouldFetch 为 false 时则不会取数。

    第一个取数参数推荐为回调函数,这样 swr 会 catch 住内部异常,比如:

    // ... or throw an error when user.id is not defined
    const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);

    如果 user 对象不存在,user.id 的调用会失败,此时错误会被 catch 住并抛到 error 对象。

    实际上,user.id 还是一种依赖取数场景,当 user.id 发生变化时需要重新取数。

    2.5 依赖取数

    如果一个取数依赖另一个取数的结果,那么当第一个数据结束时才会触发新的取数,这在 swr 中不需要特别关心,只需按照依赖顺序书写 useSWR 即可:

    function MyProjects() {
      const { data: user } = useSWR("/api/user");
      const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
    
      if (!projects) return "loading...";
      return "You have " + projects.length + " projects";
    }

    swr 会尽可能并行没有依赖的请求,并按依赖顺序一次发送有依赖关系的取数。

    可以想象,如果手动管理取数,当依赖关系复杂时,为了确保取数的最大可并行,往往需要精心调整取数递归嵌套结构,而在 swr 的环境下只需顺序书写即可,这是很大的效率提升。优化方式在下面源码解读章节详细说明。

    依赖取数是自动重新触发取数的一种场景,其实 swr 还支持手动触发重新取数。

    2.6 手动触发取数

    trigger 可以通过 Key 手动触发取数:

    import useSWR, { trigger } from "swr";
    
    function App() {
      return (
        <div>
          <Profile />
          <button
            onClick={() => {
              // set the cookie as expired
              document.cookie =
                "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
    
              // tell all SWRs with this key to revalidate
              trigger("/api/user");
            }}
          >
            Logout
          </button>
        </div>
      );
    }

    大部分场景不必如此,因为请求的重新触发由数据和依赖决定,但遇到取数的必要性不由取数参数决定,而是时机时,就需要用手动取数能力了。

    2.7 乐观取数

    特别在表单场景时,数据的改动是可预期的,此时数据驱动方案只能等待后端返回结果,其实可以优化为本地先修改数据,等后端结果返回后再刷新一次:

    import useSWR, { mutate } from "swr";
    
    function Profile() {
      const { data } = useSWR("/api/user", fetcher);
    
      return (
        <div>
          <h1>My name is {data.name}.</h1>
          <button
            onClick={async () => {
              const newName = data.name.toUpperCase();
              // send a request to the API to update the data
              await requestUpdateUsername(newName);
              // update the local data immediately and revalidate (refetch)
              mutate("/api/user", { ...data, name: newName });
            }}
          >
            Uppercase my name!
          </button>
        </div>
      );
    }

    通过 mutate 可以在本地临时修改某个 Key 下返回结果,特别在网络环境差的情况下加快响应速度。乐观取数,表示对取数结果是乐观的、可预期的,所以才能在结果返回之前就预测并修改了结果。

    2.8 Suspense 模式

    在 React Suspense 模式下,所有子模块都可以被懒加载,包括代码和请求都可以被等待,只要开启 suspense 属性即可:

    import { Suspense } from "react";
    import useSWR from "swr";
    
    function Profile() {
      const { data } = useSWR("/api/user", fetcher, { suspense: true });
      return <div>hello, {data.name}</div>;
    }
    
    function App() {
      return (
        <Suspense fallback={<div>loading...</div>}>
          <Profile />
        </Suspense>
      );
    }

    2.9 错误处理

    onErrorRetry 可以统一处理错误,包括在错误发生后重新取数等:

    useSWR(key, fetcher, {
      onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
        if (retryCount >= 10) return;
        if (error.status === 404) return;
    
        // retry after 5 seconds
        setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
      }
    });

    1. 本地突变
    2. suspense mode
    3. 错误处理
    // conditionally fetch
    const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
    
    // ...or return a falsy value
    const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
    
    // ... or throw an error when user.id is not defined
    const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher);

    广州品牌设计公司https://www.houdianzi.com PPT模板下载大全https://redbox.wode007.com

    3 精读

    3.1 全局配置

    在 Hooks 场景下,包装一层自定义 Context 即可实现全局配置。

    首先 SWRConfig 本质是一个定制 Context Provider:

    const SWRConfig = SWRConfigContext.Provider;

    在 useSWR 中将当前配置与全局配置 Merge 即可,通过 useContext 拿到全局配置:

    config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);

    3.2 useSWR 的一些细节

    从源码可以看到更多细节用心,useSWR 真的比手动调用 fetch 好很多。

    兼容性

    useSWR 主体代码在 useEffect 中,但是为了将请求时机提前,放在了 UI 渲染前(useLayoutEffect),并兼容了服务端场景:

    const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;

    非阻塞

    请求时机在浏览器空闲时,因此请求函数被 requestIdleCallback 包裹:

    window["requestIdleCallback"](softRevalidate);

    softRevalidate 是开启了去重的 revalidate:

    const softRevalidate = () => revalidate({ dedupe: true });

    即默认 2s 内参数相同的重复取数会被取消。

    性能优化

    由于 swr 的 data、isValidating 等数据状态是利用 useState 分开管理的:

    let [data, setData] = useState(
      (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
    );
    // ...
    let [isValidating, setIsValidating] = useState(false);

    而取数状态变化时往往 data 与 isValidating 要一起更新,为了仅触发一次更新,使用了 unstable_batchedUpdates 将更新合并为一次:

    unstable_batchedUpdates(() => {
      setIsValidating(false);
      // ...
      setData(newData);
    });

    其实还有别的解法,比如使用 useReducer 管理数据也能达到相同性能效果。

    3.3 初始缓存

    当页面切换时,可以暂时以上一次数据替换取数结果,即初始化数据从缓存中拿:

    const shouldReadCache = config.suspense || !useHydration();
    
    // stale: get from cache
    let [data, setData] = useState(
      (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
    );

    上面一段代码在 useSWR 的初始化期间,useHydration 表示是否为初次加载:

    let isHydration = true;
    
    export default function useHydration(): boolean {
      useEffect(() => {
        setTimeout(() => {
          isHydration = false;
        }, 1);
      }, []);
    
      return isHydration;
    }

    3.4 支持 suspense

    Suspense 分为两块功能:异步加载代码与异步加载数据,现在提到的是异步加载数据相关的能力。

    Suspense 要求代码 suspended,即抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件。

    核心代码就这一段,抛出取数的 Promise:

    throw CONCURRENT_PROMISES[key];

    等取数完毕后再返回 useSWR API 定义的结构:

    return {
      error: latestError,
      data: latestData,
      revalidate,
      isValidating
    };

    如果没有上面 throw 的一步,在取数完毕前组件就会被渲染出来,所以 throw 了请求的 Promise 使得这个请求函数支持了 Suspense。

    3.5 依赖的请求

    翻了一下代码,没有找到对循环依赖特别处理的逻辑,后来看了官方文档才恍然大悟,原来是通过 try/catch + onErrorRetry 机制实现依赖取数的。

    看下面这段代码:

    const { data: user } = useSWR("/api/user");
    const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);

    怎么做到智能按依赖顺序请求呢?我们看 useSWR 取数函数的主体逻辑:

    try {
      // 设置 isValidation 为 true
      // 取数、onSuccess 回调
      // 设置 isValidation 为 false
      // 设置缓存
      // unstable_batchedUpdates
    } catch (err) {
      // 撤销取数、缓存等对象
      // 调用 onErrorRetry
    }

    可见取数逻辑被 try 住了,那么 user.id 在 useSWR("/api/user") 没有 Ready 的情况一定会抛出异常,则自动进入 onErrorRetry 逻辑,看看下次取数时 user.id 有没有 Ready。

    那么什么时候才轮到下次取数呢?这个时机是:

    const count = Math.min(opts.retryCount || 0, 8);
    const timeout =
      ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;

    重试时间基本按 2 的指数速度增长。

    所以 swr 会优先按照并行方式取数,存在依赖的取数会重试,直到上游 Ready。这种简单的模式稍稍损失了一些性能(没有在上游 Ready 后及时重试下游),但不失为一种巧妙的解法,而且最大化并行也使得大部分场景性能反而比手写的好。

  • 相关阅读:
    [Leetcode] generate parentheses 生成括号
    [Leetcode] letter combinations of a phone number 电话号码的字母组合
    MySQL安装出现“不是内部或外部命令,也不是可执行程序”等一系列问题的解决方案
    [Leetcode] sudoku solver 求解数独
    [Leetcode] valid sudoku 有效数独
    jQuery使用(七):事件绑定与取消,及自定事件的实现原理
    前端交互体验核心之事件(一)
    jQuery使用(六):DOM操作之元素包裹、克隆DOM与data的综合应用
    jQuery使用(五):DOM操作之插入和删除元素
    jQuery使用(四):DOM操作之查找兄弟元素和父级元素
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14096953.html
Copyright © 2011-2022 走看看