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

所有代码已经包含到 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结果。

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

相关推荐
Qian Xiaoo9 分钟前
前后端分离开发 和 前端工程化
前端
要加油哦~24 分钟前
vue · 插槽 | $slots:访问所有命名插槽内容 | 插槽的使用:子组件和父组件如何书写?
java·前端·javascript
先做个垃圾出来………33 分钟前
split方法
前端
前端Hardy1 小时前
HTML&CSS:3D图片切换效果
前端·javascript
spionbo1 小时前
Vue 表情包输入组件实现代码及完整开发流程解析
前端·javascript·面试
全宝1 小时前
✏️Canvas实现环形文字
前端·javascript·canvas
lyc2333331 小时前
鸿蒙Core File Kit:极简文件管理指南📁
前端
我这里是好的呀1 小时前
全栈开发个人博客12.嵌套评论设计
前端·全栈
我这里是好的呀2 小时前
全栈开发个人博客13.AI聊天设计
前端·全栈