这个服务器明明实力超强请求却过分谦虚

节流和防抖本身是一件非常简单的事,但是翻遍大部分博客,很少能找见,对节流和防抖做了相关优化的。所以本篇主要讲述一下为什么要做优化,以及具体如何实现优化。
首先,为还没学过节流和防抖的猿同胞解释一下最基本的概念和实现:

防抖

假设页面中有一个 div,为这个 div 绑定一个监听鼠标移动的事件,比如这样。

let index = 0;
document
  .querySelector("div")
  .addEventListener("mousemove", () => console.log(++index));

那当鼠标在该 div 上移动时,鼠标移动事件会被疯狂执行,在很短时间内,输出的 index 就可能会飙到一百多,类似的高频操作还包括用户不断下拉刷新、监听浏览器滚动等。而在实际情况下,回调事件的代码可能是非常复杂的,包括但不仅限于不断向服务端发送请求。所以为了应对这种情况,可以利用防抖,减少回调事件的执行内容。

防抖(debounce):在函数需要频繁触发时,只有当有足够空闲的时间时,才执行一次。即在第一次触发事件时,不立即执行函数,而是给出一个延迟时间,如果在延迟时间内没有再次触发事件,那么就执行函数;如果在延迟时间内再次触发事件,那么当前的计时取消,重新开始计时等待执行。

效果:如果短时间内大量触发同一事件,只会执行一次函数。

实现:只需定义一个计时器,每次回调事件触发时,清除计时器再重新设置即可。同时为了保证回调函数传入的参数不丢失,这里采用扩展运算符,将参数全部传入最终需要执行的事件中。

const debounce = (callback, delay) => {
  let timer = null;
  return (...params) => {
    clearTimeout(timer);
    timer = setTimeout(() => callback(...params), delay);
  };
};

let index = 0;
document.querySelector("div").addEventListener(
  "mousemove",
  debounce(() => console.log(++index), 3000)
);

这里延迟 3s 是为了方便展示效果,通过这种方式,index 就不会被高频输出了,只有当事件监听不再触发的 3s 之后,才会输出一次。

我看到很多博客到这里就结束了,但这里其实还有个小问题。也许在实际情况中,需要执行的事件确实远比 clearTimeout、setTimeout 复杂和很多,但其实短时间内数百次地疯狂清除再设置计时器,也是一件非常消耗性能的事,为了再解决这一点就需要优化一下实现思路。

优化实现:既然不希望高频地清除重置计时器,那么就只好不清除,让当前设置的计时器执行,但是计时器的回调触发后,为了不让最终需要执行的事件被触发,就需要有个时间戳,判断当前时间下是否该执行。如果该执行,就清掉计时器并执行;如果不该执行,就单纯清掉计时器,重新设置计时器,这样就能大幅减少计时器的清除和设置。而每次移动事件被触发,只需要判断当前是否有计时器,没有就设置,有则不需要做任何事。

const debounce = (callback, delay) => {
  let timer = null,
    stop = null;
  const runner = (...params) => {
    clearTimeout(timer);
    const start = Date.now();
    timer = setTimeout(() => {
      const now = Date.now();
      if (now >= stop) {
        clearTimeout(timer);
        timer = null;
        callback(...params);
      } else runner(...params);
    }, stop - start);
  };
  return (...params) => {
    stop = Date.now() + delay;
    if (!timer) runner(...params);
  };
};

let index = 0;
document.querySelector("div").addEventListener(
  "mousemove",
  debounce(() => console.log(++index), 3000)
);

虽然代码看起来复杂了,但对于浏览器来说,高频执行的只有 stop 复制和判断语句,远比清除重置计时器要节省性能。

节流

同样是假设对于绑定了 mousemove 事件的 div,换一种思路。当鼠标在 div 上移动时,不等到最后一次结束后再等待延迟时间后执行,而是每固定一个时间段就执行一次。比如每 1s 执行依次,那如果鼠标在 div 上滑动时间大于 9s 而小于 10s,就执行 10 次。

节流(throttle):预定一个函数只有在大于等于执行周期时才执行,周期内调用不执行。而最终触发时,用的是周期内最后一次监听所产生的参数。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现:只需定义一个计时器,每次回调事件触发时,判断计时器是否已经存在,而当到达一个周期执行后,清除定时器和存储参数即可。

const throttle = (callback, wait) => {
  let timer = null,
    save = undefined;
  return (...params) => {
    save = params;
    if (!timer)
      timer = setTimeout(() => {
        clearTimeout(timer);
        callback(...save);
        timer = null;
        save = undefined;
      }, wait);
  };
};

let index = 0;
document.querySelector("div").addEventListener(
  "mousemove",
  throttle(() => console.log(++index), 3000)
);

对于节流,因为没有高频的定时器的清除和重置,就不需要做什么优化了。这里想提出一个有关异步的节流问题:假设需要调用 100 个 API 获取数据,并且需要越快越好。如果使用 Promise.all,100 个请求会同时到达服务器,如果服务器性能很低的话,这就会是个负担。那么就需要节流 API 请求,假设任何时刻最多只能有 5 个请求在服务器正在进行中,需要实现一个 throttlePromise 函数,接受一个 promise 函数组成的数组,和一个同时刻可进行的 API 最大数量,返回结果达成和 Promise.all 完全相同。

放一个我搜罗诸多博客后,自认为最佳的解决办法:先直接发送 max 个请求,当其中有任意一个请求返回后,立即补充发送下一个请求,也就是第 max + 1 个,这样服务器中就又同时存在 max 个请求,然后以此类推。

const throttlePromise = (promises, max) => {
  const result = [];
  const runner = async (iterator) => {
    for (let [index, item] of iterator) {
      result[index] = await item();
    }
  };
  const iterator = Array.from(promises).entries();
  const workers = Array(max).fill(iterator).map(runner);
  return Promise.all(workers).then(() => result);
};

如果对迭代器了解不充分的话,这个方法的实现逻辑就不好理解了,可以先学习一下迭代器相关知识。

首先这里对 promises 做了一层拷贝后,通过 entries 方法获取了数组的一个迭代器,这个迭代器每次调用 next 方法,返回的对象的 value 属性,是由原数组的 index 和 value 组成的数组。再创建一个长度为 max 的数组,每个元素都指向这个迭代器,由于迭代器是个复杂对象,所以数组的每一项都是同一个迭代器(严格来讲其实是数组每一项存入了这个遍历器的地址),即若 const iterators = Array(max).fill(iterator),则有 iterators[i] = iterators[j],对任意 i 和 j 小于 max 成立,这样再调用 map 时,max 次循环都会把同一个迭代器传给 runner 函数,由于 runner 时 async 函数,返回值自然回是一个 Promise,因此 workers 就可以托给 Promise.all 监听。

在每个 runner 函数内部,for of 循环每遍历一次 iterator,就相当于调用一次 iterator 的 next 方法,将该遍历器的指针指向下一个,当 iterator 指针指向最后一个之后,也就是再调用 next 方法会返回{ value: undefined, done: true }时,for of 循环就会结束,整个 runner 函数也就执行完毕了。但因为 map 调用了 max 次 runner 函数,每次传入的都是同一个遍历器,那么只要当其中一次调用的 runner 函数将遍历器的指针移向了下一项,max 次调用的 runner 函数的遍历器指针也都会随之指向了下一项,同时只要当其中一次移过了最后一项,max 次就都会结束 for of 循环。

因此,可以保证服务端永远都会有 max 个请求,而且 promises 也只遍历了一次,当最后一个请求返回时,workers 中的 max 个 Promise 全部结束(非同时结束,因为每次调用的 runner 中遍历到的 API 是完全不同的),然后将 result 返回给外界即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 一、Web端 https://www.nowcoder.com/discuss/588372 1.float如何清...
    陈一季阅读 4,224评论 2 18
  • 在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负...
    陈光环_18阅读 209评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续...
    刘其瑞阅读 2,567评论 0 1
  • Debounce 为什么要去抖动?我们知道 浏览器有一些原生事件,比如 resize scroll keyup k...
    指尖跳动阅读 199评论 0 1