因为没有对按钮连击和接口调用频率做限制,客户集体爆仓了!

3,047 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

背景

这个项目是一个货币交易客户端,后端会走币安的开放接口,而币安的接口每分钟调用次数是有阈值的,调多了直接接口返回错误。

客户端里,有的窗口可能涉及 多个信息的查询 ,而这些信息需要调用不同的币安的接口,因此后端有的接口调用起来 权重很大(存在一个接口需要调用币安十几个接口的情况)。

那么接口调用权重大的有两个窗口,其中一个是账户信息窗口。

账户信息窗口需要实时的更新持仓盈亏以及强平价、开仓价等信息,这些信息分布在币安各个接口里,所以调用这个接口的 权重很大 。在这个窗口中我们添加了一个 强制刷新数据 按钮,用来 防止行情波动大 时卡住,影响 数据实时性

那么当时的我还是欠考虑,忘记 给按钮添加防抖操作了,带来的结果就是在网络状况不好的情况下,有些比较急躁的用户会 连击 ,这样会一直调用接口,权重很快就达到阈值了。达到阈值后平仓平不了,亏钱甚至是爆仓,只能干瞪眼。

所以我们要 控制用户连击行为 ,这就要用到节流了。

另一个调用权重大的窗口是交易窗口,委托下单成功后会推送持仓数据、开仓价等。委托单有几个状态:挂单、部成(部分成交,多次)或者已成(完全成交,一次),部成状态和已成状态都会推送数据,有推送就要调接口。那么部成的情况下就很容易短时间内(0.5s)达到完全成交,也就是说有可能 一个委托单会触发好几次的接口调用 。这种客户端主要功能就是 下单 了,行情波动大的时候交易员都是快捷键操作,一秒几单,这是达到阈值的主要原因,不节流等着提桶吧。

微信图片_20201123155922.jpg

节流是什么

介绍了这么多,有的小伙伴还不知道什么是“节流”,或者是听过 防抖节流 ,但是一直对这两个概念混淆,接下来我额外给大家做个小科普。

想必很多人都有玩过 moba 游戏,我拿大众点的 英雄联盟王者荣耀 来举例。

节流:英雄是会释放技能的,技能释放完会有冷却 cd,如果没有冷却完毕,不管你手按的再快,技能都放不出来。这个就是节流,一定时间疯狂连击我只触发一次。

防抖:回城都知道吧,王者荣耀里回城所需的时间是 7 秒,如果在回城过程中你再次点击回城,那么回城时间是会被重置的。比如你点击回城过了 3 秒了,这个时间手欠又点了一下回城,好了,原本只要再等 3 秒就能泡泉水,这下你又要重新登 7 秒了。这个就是防抖。

微信图片_20220818195830.jpg

回归正题,因为我希望的是 允许用户刷新,但是不能太频繁,最好是一段时间内只允许刷新一次 ,是不是和上面节流的例子一样,妥妥的节流就安排上了嘛。

如何节流

不使用节流

我们先使用一个简单的例子来讲。

逻辑就是鼠标在灰色 box 上移动时,不断递增数字。

<style>
    .box {
        background-color: grey;
        height: 100px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 20px;
        color: #fff;
    }
</style>
<body>
    <div class="box" id="box">0</div>
    <script>
        const box = document.querySelector('#box');
        let count = 0;
        box.addEventListener('mousemove', ()=>{
            box.innerHTML = ++count;
        })
    </script>
</body>

20220928_110435.gif

可以看到,正常情况下 mousemove 事件会频繁触发。如果换成接口调用会咋样?想都不敢想。

微信图片_20220727181614.jpg

使用节流之后

我们的需求还是鼠标移动时,数字递增,不同的是我们希望数字增长不要太快(事件触发频繁不要太快),这就要用到防抖了。

我们来改造一下代码:

    const box = document.querySelector('#box');
    let count = 0;
    const throttle = (callback) => {
        let time = 0;
        return () => {
            const now = Date.now();
            const diff = (now - time) / 1000;
            if(diff > 0.5) {
                callback();
                time = now;
            }
        }
    }
    box.addEventListener('mousemove', throttle(()=>{
        box.innerHTML = ++count;
    }))

其中,throttle 函数的返回值是一个函数,这个函数引用了外层变量 time,形成了一个闭包。

变量 time 用来记录 上一次调用发生的时间 ,一开始默认为 0 ,这样下次触发就能 直接进行第一次调用

后续触发事件回调时,判断当前触发回调的时间和上一次触发回调的 时间差 是不是 大于 我们规定的时间(0.5s),如果大于则允许调用,否则本着节流的逻辑,这次调用显然不被允许了。

需要注意的是,在允许调用的情况下,我们要 更新 time 的值为 now

我们来看看改造后的效果:

20220928_112237.gif

模板

相信大家都看出来了,朴素的节流有一套模板:

const thrrotle = (callback) => {
    let time = 0;
    return () => {
        const now = Date.now();
        const diff = (now - time) / 1000;
        if(diff > 0.5) {
            callback();
            time = now;
        }
    }
}

还有种节流是通过一个 flag 变量控制是否允许调用回调的:

 function throttle(fn,delay) {
    let flag = true;
    return function() {
        if (flag) {
            setTimeout(() => {
                fn.call(this); // 绑定 this
                flag = true;
            }, delay);
        }
        flag = false;
    }
}

示例

那么我项目中就是控制 10 秒内只允许触发一次接口调用,因此这里的 0.5 我要改成 10。

// 点击刷新按钮尝试刷新
const attempRefresh = (() => {
  let lastTime = new Date().getTime();
  const delay = 10;
  return () => {
    const now = new Date().getTime();
    const diff = (now - lastTime) / 1000;
    if (diff >= delay) {
      getAccountInfo(); // 调用接口
      lastTime = now;
    } else {
      message.info({
        content: `刷新过于频繁,请${delay - Math.floor(diff)}秒后尝试!`,
        key: EMessageKey.ACCOUNT_INFO,
      });
    }
  };
})();

经过这么一改造,用户第一次点击刷新的时候是允许刷新的,而在 10 秒内妄图再次刷新,展现给它的只有冰冷的提示语。

微信图片_20220824182841.jpg

结束语

日常开发中,除了限制接口调用频率外,像页面 scroll 事件、窗口 resize 事件,为了性能考虑,都是需要进行节流处理的,而看完本文,相信大家都理解掌握了节流的方法,套用模板就完事了。但是还是希望大家能吃透,毕竟代码也不多,有了思路就不用去背代码了。学到就是赚到。