Skip to content

当节流遇到异步 #3

Description

@bowencool

2021.05.31 更新:
所有代码已经包含到 https://github.com/bowencool/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应该不会被发现吧🌚,可是万一接口要是挂了...算了,来不及想这些了

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

能不能站着就把钱挣咯?

我们先来梳理一下:

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

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

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

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

第二点,promise的响应时间是不确定的,我们希望上一个promise结束之前,接下来的promise创建操作统统丢弃。所以,“短时间内”就等于“上一个promise的pending期间”,“接下来的promise创建操作统统丢弃”意思就是“取第一次”,promise的丢弃可以创建“永远pending”的promise,所以我们的需求就是:
在上一个promise的pending期间,多次promise创建操作中取第一次(就是这个正在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
)

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

debounceAsync

刚才那个是创建promise前,那么已经创建了很多promise,到底怎么才能取到最新的结果呢,毕竟哪个promise跑得快,谁也不知道。所以就会有debounceAsync(Demo):
已经创建的众多promise中,取最后创建的promise结果。

/**
 * @description 异步去抖:短时间内触发多次,取最后一次触发的结果。如果说debounce是发送前取最后一次输入,那么debounceAsync就是发送后取最后一次请求对应的输出。
 * @example https://bowencool.github.io/async-utilities/
 * @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);
      });
  };
}

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

Activity

  1. pinned this issue on May 25, 2020
  2. added a commit that references this issue on Jan 6, 2024
  3. Repository owner locked and limited conversation to collaborators on Jan 19, 2024
  4. converted this issue into a discussion #55 on Jan 19, 2024
  5. unpinned this issue on Jan 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions