likes
comments
collection
share

不用防抖和节流,用更底层的方式解决JS的重复请求

作者站长头像
站长
· 阅读数 19

欢迎转载,评论,然后注明来源和github地址即可。

如果你认为本文或本工具对你有帮助,请点赞收藏或给项目star,这对我真的很重要。ヾ(•ω•`)o

项目 github 地址

你或许在项目中遇到过这样的情况。

  1. 成员A成员B都用得上一个后端接口api,但它们互相不知道对方什么时候请求这个接口,因此导致一打开页面,同一个接口竟然重复请求了多次。
  2. 由于用户手抖,又因为成员忘记做请求的loading防误触处理,导致一个接口被用于疯狂请求,最终数据乱套,页面不可用。
  3. SPA单页面应用,多个页面甚至是多个组件可能有同样的数据请求,完全可以共享的数据却不得不重复请求,影响页面加载效率。
  4. 想要用节流或者防抖解决上面的问题,但是后端返回数据的时间浮动太大,导致不知道应该设置多长的时间。

这些请求浪费,实际上都有调用异步函数(async function)的参与的;因此,它们虽不是async function的问题,但却可以利用async function的特点来解决。

async function本质上是一个Promise。因此只要利用好Promise的特性,就能解决这些问题。

once-init 正是为解决这些问题而生。它从 Promise 的定义出发,用 Promise 的基础功能彻底地阻止了异步请求浪费的发生。

我用它做了两件事:

  1. 缓存请求的返回值;
  2. 缓存Promise请求本身

原理

once-init 的核心思想是缓存和执行队列;

缓存返回值

实现缓存返回值并不困难,只要写一个单例模式就好了。下面是一个缓存的单例模式的简单示例;

class OnceInit {
    cache = undefined;
    func;
    
    constructor(asyncFunc) {
        this.func = asyncFunc;
    }

    async init() {
        if (typeof this.cache !== 'undefined') {
            return this.cache;
        } else {
            const res = await this.func();
            this.cache = res;
            return res;
        }
    }
}

// 使用
const oiFoo = new OnceInit(someAsyncFunc);
await oiFoo.init();
  1. 如果缓存已经有值,返回缓存的值;
  2. 如果缓存没有值,执行异步函数;执行完毕后,更新缓存;

这是一个简易的解决方案,它大概能解决10%的异步函数相关的问题,因为在第一次执行Promise完成之后,就不会再进行请求,也就不会产生浪费了;

但是,它没有解决多个Promise同时发生的情况。

假设开发人员同一时间多次调用init,如果第一次调用的Promise还没有完成,cache也还没有初始化,就会导致同一时间的所有调用依旧创建新的Promise

甚至有可能因为多次请求,不断的变化cache,你甚至没有办法确定最后cache的值是不是你最后一次请求的返回值。

如果要解决这个问题,就需要利用Promise的特性,同一时间,同一个async function,只允许同一个Promise处在pending状态

缓存 Promise

  1. 如果Promise正在执行,就不创建新的Promise;直接返回正在执行的Promise的返回值;
  2. 如果没有Promise正在执行,就创建并缓存新的Promise
    1. Promise执行结束之后,删除缓存的Promise
class OnceInit {
    cache = undefined;
    func;
    promise = undefined;
    constructor(asyncFunc) {
        this.func = asyncFunc;
    }

    async init() {
        if (typeof this.cache !== 'undefined') {
            return this.cache;
        } else {
            if (this.promise) {
                return await this.promise;
            } else {
                const promise = this.func();
                promise.finally(() => {
                    this.promise = undefined;
                })
                this.promise = promise;
                const res = await promise;
                this.cache = res;
                return res;
            }
        }
    }
}

通过这种方式,就能避免promise同一时间重复执行。这也是once-init这个库的核心思想。

当然这个简单实现还有很多问题需要解决。

很多伙伴都对我的实现提出了可靠的建议,为我修正改进项目帮助很大。经过这一段时间的打磨,并在实际生产环境中试用了一段时间后,现在终于推出了它的v1.0.0正式版本了。


once-init

🗼 Makes asynchronous function execution manageable.

封装可控的 async function

你可以让同一个 async function 不会在同一时间内被执行两次,以防止发出重复的请求。

你可以让第二次执行 async function ,直接返回第一次执行的结果,而不是重复执行函数。

解决大量的相同请求的问题。

详细且精确的 Typescript 检查。

安装

npm install once-init

简介

once-init 的核心思想是缓存和执行队列;

使用

// 0. 引入once-init
import oi from "once-init";

// 1. 创建一个异步函数
async function foo() {
  // do something, for example, request backend data.
  const res = await axios.get("xxx.com");
  return res;
}

// 2. 用once-init封装这个异步函数
const oiFoo = oi(foo);

// 3. 执行封装后的函数
oiFoo.init();

用例

一个结合axios的简单示例

import oi from "once-init";
axios.get = oi(axios.get).refresh;

只用一行,就能在调用axios.get的时候就能阻止同一时间的重复请求了。

不用 once-init

// 我们假设 axios.get("xxx.com") 返回的值是一个递增的数字,即第1次请求,会返回1,第2次请求会返回2,第n次请求会返回n。
await foo(); // 返回 1
await foo(); // 返回 2
await foo(); // 返回 3

使用 once-init

// once-init 会将重复执行重定向到第一次执行的结果上;(第一次执行后会缓存执行结果,类似单例模式)
await oiFoo.init(); // 返回 1
await oiFoo.init(); // 返回 1
await oiFoo.init(); // 返回 1

这意味着无论重复执行 oiFoo.init 多少次,foo 都只会执行第一次,返回第一次执行的结果;(就像缓存一样)

await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1]
await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1]
// 通常,如果你只会使用到init,你可以直接把 oiFoo 定义成 init 函数
const oiFoo = oi(foo).init;

await oiFoo();

如果你不使用缓存,只是希望防止同一时间发出重复请求,你可以使用refresh

// refresh和init在同一时间执行多次,都会阻止重复执行,多余的async function会返回第一次的结果;
await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [1, 1, 1]
// 但refresh如果当前没有其它重复的async function在执行,会刷新结果,并同时刷新缓存(影响到下一次init的返回);
await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [2, 2, 2]
await oiFoo.init(); // 返回 2

once-init 会区分参数,如果传入的异步函数有参,那么传入不同的参数将被视为两个不同的异步函数,不会共享缓存和执行队列;(使用lodash.isEqual判断参数是否相等)

下面这个复杂用例将会给你提供灵感:

// 假设 xxx.com/+ 会返回正数, xxx.com/- 会返回负数,两者有独立的缓存,且绝对值都递增
async function foo(op: "+" | "-") {
  const res = await axios.get(`xxx.com/${op}`);
  return res;
}

const oiFoo = oi(foo);
await oiFoo.init("-"); // 返回 -1
await oiFoo.refresh("-"); // 返回 -2
await oiFoo.refresh("-"); // 返回 -3

await oiFoo.refresh("+"); // 返回 1
await oiFoo.init("-"); // 返回 -3

更多问题和api请到项目 github 地址中查看,如果有任何问题,也请在下面评论留言或到github提交issue(热烈欢迎到github提交issue捏)。