Skip to content

当节流和去抖遇到异步,它们会擦出怎样的火花?

发布于  at 18:14

所有代码已经包含到 async-utilities 仓库中,并且已发布到 npm

背景

HTML的表单里,有这样一种场景:

<form id="form">
  <!-- <label for="name">Name:</label>
  <input type="text" name="name" id="name"> -->
  <button type="submit">submit</button>
</form>

点击“提交”就往服务端发送一个请求:

// 网络请求
function api(data) {
  console.log("submiting", data);
  return fetch("https://httpbin.org/delay/1.5", {
    body: JSON.stringify(data),
    method: "POST",
    mode: "cors",
  });
}

const form = document.getElementById("form");
const handler = async function (e) {
  e.preventDefault();
  const rez = await someApi({
    msg: "some data to be sent",
  });
  console.log(rez);
};

form.addEventListener("submit", handler);

为防止用户重复提交,我们通常会维护一个loading状态…但是写得多了,难免有一种机械劳动的感觉。而且,当一个表单出现很多按钮时,我岂不是维护很多loading变量?

我看着眼睛好累,而且接口响应很快,偷偷少写一个loading应该不会被发现吧🌚,可是万一接口要是挂了…算了,来不及想这些了

上面的场景不知道你有没有经历过呢?实际上,大多数产品的按钮都是没有loading效果的,因为整个世界就是一个大草台班子😂,但是作为一个合格的前端,每个人都需要对用户体验负责!

能不能站着就把钱挣咯?

我们先来梳理一下:

  1. 短时间内每个事件都会产生一个 promise,核心需求是降频。即“promise三千,我只取一个结果”
  2. promise 的响应时间是不确定的

降频

先回想一下同步代码中事件降频:节流(throttle)、防抖(debounce)。关于这两者,相信你已经很熟悉了,我们一句话概括:

二者都是在单位时间内的多次相同事件中取一次调用(也可以说成:事件三千,我只取一次执行),不同的是前者取的第一次,后者取的最后一次

把我们的需求也改成这种句式:在短时间内的多次相同事件中取一次调用。所以,这个“短时间内”才是关键 !

重新定义间隔

我们希望上一个promise结束之前,接下来的promise创建操作统统丢弃。所以,“短时间内”就等于“上一个promisepending期间”,“接下来的promise创建操作统统丢弃”意思就是“取第一次”,promise的丢弃可以通过创建“永远pending”的promise实现,所以我们的需求就是: 在上一个promisepending期间,多次promise创建操作中取第一次(就是这个正在pendingpromise)执行。

编码

思路都参考了,代码也参考一下吧,这里贴个简易版的节流实现代码:

/**
 * @description 节流
 * @param {function} fn
 * @param {number} ms 毫秒
 * @returns {function} 节流后的function
 */
function throttle(fn, ms = 300) {
  let lastInvoke = 0;
  return function throttled(...args) {
    const now = Date.now();
    if (now - lastInvoke < ms) return;
    lastInvoke = now;
    fn.call(this, ...args);
  };
}

依葫芦画瓢,简单改造一下:

/**
 * @description 异步节流:上一次的promise pending期间,不会再次触发
 * @param {() => Promise<any>} fn
 * @returns {() => Promise<any>} 节流后的function
 */
function throttleAsyncResult(fn) {
  let isPending = false;
  return function (...args) {
    if (isPending) return new Promise(() => {});
    isPending = true;
    return fn
      .call(this, ...args)
      .then((...args1) => {
        isPending = false;
        return Promise.resolve(...args1);
      })
      .catch((...args2) => {
        isPending = false;
        return Promise.reject(...args2);
      });
  };
}

使用方法(Demo)

以下 Demo 以网络请求为例,打开 Devtool 查看效果。


查看代码
import { throttleAsyncResult } from "@bowencool/async-utilities";
/* make a network request */
function api(data: { msg: string }) {
  console.log("submiting", data);
  return fetch("https://httpbin.org/delay/1.5", {
    body: JSON.stringify(data),
    method: "POST",
    mode: "cors",
  });
}

const throttledApi = throttleAsyncResult(api);

export default function ThrottleAsyncResultDemo() {
  return (
    <button
      onClick={async function () {
        const rez = await throttledApi({
          msg: "some data to be sent",
        });
        console.log("completed");
      }}
    >
      submit(click me quickly)
    </button>
  );
}

打开开发者工具可以看到,无论点击多快,始终不会出现请求并行的情况: image 大功告成!

一个孪生兄弟

debounceAsyncResult

刚才的throttleAsyncResult是控制如何创建promise,那么如果已经创建了很多promise,我们该如何才能取到最新的结果呢,毕竟哪个promise跑得快,谁也不知道。

所以就会有debounceAsyncResult(Demo):已经创建的众多promise中,取最后创建的promise结果。

“偷懒”是程序员第一生产力,学到了吗🤔?

分享到: