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

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

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

相关推荐
outstanding木槿4 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~11 分钟前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫15 分钟前
高德地图自定义折线矢量图形
前端·vue.js·vue
所以经济危机就是没有新技术拉动增长了15 分钟前
二、javascript的进阶知识
开发语言·javascript·ecmascript
m0_7482509317 分钟前
html 通用错误页面
前端·html
来吧~26 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
Bubluu26 分钟前
浏览器点击视频裁剪当前帧,然后粘贴到页面
开发语言·javascript·音视频
鎈卟誃筅甡39 分钟前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅44 分钟前
二、创建第一个VUE项目
前端·javascript·vue.js
m0_748239331 小时前
前端(Ajax)
前端·javascript·ajax