当节流遇到异步

2,191 阅读3分钟

第一位男子叫异步 身着西装赛徐公 他有个弟弟叫回调 但异步只爱Promise

第二位男子叫节流 是个浪漫少年 喜欢去救助加班的程序媛但更喜欢跳舞

直到有那么一天 他们决定出去闯荡世界 他们踏上了旅程 成为了人们的偶像 偶然有一天 在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应该不会被发现吧🌚,可是万一接口要是挂了...算了,来不及想这些了

上面的场景不知道你有没有经历过呢?下面我们就来探究一下:

能不能站着就把钱挣咯?

我们先来梳理一下:

  1. 短时间内每个事件都会产生一个promise,核心需求是事件去重
  2. promise的响应时间是不确定的

第一点,去重,先回想一下同步代码中事件去重:节流(throttle)、防抖(debounce)。关于这两者,相信你已经很熟悉了,我们一句话概括:二者都是在单位时间内的多次相同事件中取一次调用,不同的是前者取的第一次,后者取的最后一次。把我们的需求也改成这种句式:在短时间内的多次相同事件中取一次调用。所以,这个“短时间内”才是关键 !

第二点,promise的响应时间是不确定的,我们希望上一个promise结束之前,接下来的事件统统丢弃。所以,“短时间内”就等于“上一个promise的pending期间”,“接下来的事件统统丢弃”意思就是“取第一次”,所以我们的需求就是:在上一个promise的pending期间的多次相同事件中取第一次(就是这个正在pending的promise)调用。

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

/**
 * @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 throttleAsync(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):

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

// 模拟业务逻辑
const button = document.getElementById('button')
const handler = async function() {
  const rez = await throttledApi({
    msg: 'some data to be sent'
  })
  console.log('completed')
}

button.addEventListener(
  'click',
  handler
)

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

同理debounceAsync(Demo):

/**
 * @description 异步去抖:短时间内触发多次,取最后一次触发的结果
 * @param {() => Promise<any>} fn
 * @returns {() => Promise<any>} 去抖后的function
 */
function debounceAsync(fn) {
  let lastFetchId = 0;

  return function(...args) {
    const fetchId = ++lastFetchId;

    return fn
      .call(this, ...args)
      .then((...a1) => {
        if (fetchId !== lastFetchId) {
          return new Promise(() => {});
        } else {
          return Promise.resolve(...a1);
        }
      })
      .catch((...a2) => {
        return Promise.reject(...a2);
      });
  };
}

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

原文链接:blog.bowen.cool/zh/posts/wh…

欢迎我的公众号,佛性更新:


2021.05.31 更新:

所有代码已经包含到 github.com/bowencool/a… 仓库中,并且已发布到 npm