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

所有代码已经包含到 async-utilities 仓库中,并且已发布到 npm 原文链接:blog.bowen.cool/zh/posts/wh...

背景

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

html 复制代码
<form id="form">
  <!-- <label for="name">Name:</label>
  <input type="text" name="name" id="name"> -->
  <button type="submit">submit</button>
</form>

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

js 复制代码
// 网络请求
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)执行。

编码

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

js 复制代码
/**
 * @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);
  };
}

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

js 复制代码
/**
 * @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)

tsx 复制代码
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>
  );
}

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

一个孪生兄弟

debounceAsyncResult

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

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

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

相关推荐
鹏多多2 分钟前
Vue项目i18n国际化多语言切换方案实践
前端·javascript·vue.js
一只小风华~11 分钟前
Vue: 侦听器(Watch)
前端·javascript·vue.js
JarvanMo12 分钟前
Flutter Debug模式:每个开发者都需要的秘密武器(但大多数人用错了)
前端
GDAL12 分钟前
Knockout.js 备忘录模块详解
javascript·knockout
玲小珑23 分钟前
LangChain.js 完全开发手册(八)Agent 智能代理系统开发
前端·langchain·ai编程
蓝胖子的多啦A梦27 分钟前
【前端】VUE+Element UI项目 页面自适应横屏、竖屏、大屏、PDA及手机等适配方案
前端·javascript·elementui·html·前端页面适配
掘金安东尼28 分钟前
前端周刊431期(2025年9月8日–9月14日)
前端·javascript·github
Bear on Toilet33 分钟前
继承类模板:函数未在模板定义上下文中声明,只能通过实例化上下文中参数相关的查找找到
开发语言·javascript·c++·算法·继承
风若飞36 分钟前
npm ERR! code CERT_HAS_EXPIRED
前端·npm·node.js
北城笑笑43 分钟前
NodeJS 8 ,从 0 到 1:npm 包发布与更新全流程指南( 含多场景适配与踩坑总结 )
前端·npm·node.js·github