有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing) 和 函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

Throttle:第一个人说了算

throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

先给大家讲个小故事:现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走。

在这个故事里,“司机” 就是我们的节流阀,他控制发车的时机;“乘客”就是因为我们频繁操作事件而不断涌入的回调任务,它需要接受“司机”的安排;而“计时器”,就是我们上文提到的以自由变量形式存在的时间信息,它是“司机”决定发车的依据;最后“发车”这个动作,就对应到回调函数的执行。

总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要一位客人叫了车,司机就会为他开启计时器,一定的时间内,后面需要乘车的客人都得排队上这一辆车,谁也无法叫到更多的车。

对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

现在一起实现一个 throttle:

// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {// last为上一次触发回调的时间let last = 0// 将throttle处理结果当作函数返回return function () {// 保留调用时的this上下文let context = this// 保留调用时传入的参数let args = arguments// 记录本次触发回调的时间let now = +new Date()// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值if (now - last >= interval) {// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调last = now;fn.apply(context, args);}}
}// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

Debounce:最后一个人说了算

防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

继续讲司机开车的故事。这次的司机比较有耐心。第一个乘客上车后,司机开始计时(比如说十分钟)。十分钟之内,如果又上来了一个乘客,司机会把计时器清零,重新开始等另一个十分钟(延迟了等待)。直到有这么一位乘客,从他上车开始,后续十分钟都没有新乘客上车,司机会认为确实没有人需要搭这趟车了,才会把车开走。

我们对比 throttle 来理解 debounce:在throttle的逻辑里,“第一个人说了算”,它只为第一个乘客计时,时间到了就执行回调。而 debounce 认为,“最后一个人说了算”,debounce 会为每一个新乘客设定新的定时器。

现在一起实现一个 debounce:

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {// 定时器let timer = null// 将debounce处理结果当作函数返回return function () {// 保留调用时的this上下文let context = this// 保留调用时传入的参数let args = arguments// 每次事件被触发时,都去清除之前的旧定时器if(timer) {clearTimeout(timer)}// 设立新定时器timer = setTimeout(function () {fn.apply(context, args)}, delay)}
}// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {// last为上一次触发回调的时间, timer是定时器let last = 0, timer = null// 将throttle处理结果当作函数返回return function () { // 保留调用时的this上下文let context = this// 保留调用时传入的参数let args = arguments// 记录本次触发回调的时间let now = +new Date()// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值if (now - last < delay) {// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器clearTimeout(timer)timer = setTimeout(function () {last = nowfn.apply(context, args)}, delay)} else {// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应last = nowfn.apply(context, args)}}
}// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)document.addEventListener('scroll', better_scroll)

在 Vue 里使用 lodash 中的 Debouncing 和 Throttling

事件节流和防抖是提高性能或降低网络开销的好方法。虽然 Vue 1曾经支持对事件的节流和防抖,但是在Vue 2中为了保持核心的简单性,删除对事件的节流和防抖的支持。因此,在Vue 2对对事件进行防抖和节流我们可以使用 lodash 来做。

安装

可以通过 yarn 或 npm 安装 lodash。

# Yarn
$ yarn add lodash
# NPM
$ npm install lodash --save

注意:如果我们不想导入lodash的所有内容,而只导入所需的部分,则可以通过一些Webpack构建自定义来解决问题。还可以使用lodash.throttlelodash.debounce等软件包分别安装和导入lodash的各个部分。

throttling 方法

要对事件进行节流处理方法非常简单,只需将要调用的函数包装在lodash的_.throttle函数中即可。

<template><button @click="throttledMethod()">Click me as fast as you can!</button>
</template><script>
import _ from 'lodash'export default {methods: {throttledMethod: _.throttle(() => {console.log('I get fired every two seconds!')}, 2000)}
}
</script>

debouncing 方法

尽管节流在某些情况下很有用,但一般情况我们经常使用的是防抖。防抖实质上将我们的事件分组在一起,并防止它们被频繁触发。要在Vue组件中使用节流,只需将要调用的函数包装在lodash的_.debounce函数中。

<template><button @click="throttledMethod()">Click me as fast as you can!</button>
</template><script>
import _ from 'lodash'export default {methods: {throttledMethod: _.debounce(() => {console.log('I only get fired once every two seconds, max!')}, 2000)}
}
</script>

参考:

Throttling and Debouncing in JavaScript
The Difference Between Throttling and Debouncing
Examples of Throttling and Debouncing
Remy Sharp’s blog post on Throttling function calls
前端性能优化原理与实践

交流

在 Vue 中使用lodash对事件进行防抖和节流相关推荐

  1. vue中img标签onerror事件

    vue中img标签onerror事件 使用:onerror去绑定事件 该方法能正确处理onerror事件,并防止闪图 <img type="image/x-icon" :sr ...

  2. vue中使用v-on绑定事件中,获取$event.currentTarget,日志打印为null

    问题:vue中使用v-on绑定事件中,获取$event.currentTarget,日志打印为null dom结构: <li @click="clickEvent('hello',$e ...

  3. vue中el-radio-group点击事件,双击取消

    vue中el-radio-group点击事件 普通用法 需要实现双击取消 普通用法 需要注意,如果@change事件除了当前选中的单选按钮的label值,还需要带上另外的参数,则需要用event来代替 ...

  4. Vue中鼠标移入移出事件-解析

    鼠标在li上移动也会触发移出事件 两组鼠标事件 mouseover 和 mouseout mouseenter 和 mouseleave mouseover 和 mouseout 什么时候使用说明 根 ...

  5. vue中v-on支持的事件总结

    资源事件 事件名称 何时触发 error 资源加载失败时. abort 正在加载资源已经被中止时. load 资源及其相关资源已完成加载. beforeunload window,document 及 ...

  6. 010——VUE中使用lodash库减少watch对后台请求的压力

    <!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>使用 ...

  7. vue中使用组件时事件想要传递其他参数的问题

    今天在用ant design 的组件时其封装好的事件已经包含了参数,而我需要在该事件中传index判断点击的第几个元素的事件 经过查询资料结果 <a-tree-select:disabled=& ...

  8. Vue中的@blur/@focus事件

     @blur 是当元素失去焦点时所触发的事件 @focus是元素获取焦点时所触发的事件 <template><div><!--@blur 当元素失去焦点时触发blur事件 ...

  9. vue中鼠标移入移出事件种类以及区别

    1.@mouseover与@mouseout 鼠标经过时自身触发事件,其子元素同时也触发该事件. 父亲有的东西,儿子也有 ,支持冒泡. 适用于鼠标移入移出时子元素也会执行事件的场景,或者单一标签. 2 ...

  10. Vue中禁止鼠标滑轮事件

    可以通过mousewheel.prevent来执行该事件 因为我只有一个页面,所以我直接在大盒子里调用 <div class="allcontent" @mousewheel ...

最新文章

  1. c语言的特点能够编制出复杂的功能程序,以下不是C语言的特点的是()
  2. 哪个websocket库与Node.js一起使用? [关闭]
  3. Python程序全局观--以温度转换为例
  4. 13、Java Swing事件监听:事件处理模型和事件监听器
  5. objective-c 2.0编程语言,Objective-C 2.0程序设计(原书第2版) 中文PDF扫描版[15MB]
  6. python将图片转换为灰度图
  7. VTK:小部件之ContourWidget
  8. OpenCV相位校正phase corr的实例(附完整代码)
  9. php copy 文件夹,php删除与复制文件夹及其文件夹下所有文件的实现代码
  10. 《Java8实战》笔记(07):并行数据处理与性能
  11. 职场新人面试误区:我的技术好,所以你必须要请我?
  12. 梦织未来Windows驱动编程 第03课 驱动的编程规范
  13. houdini安装哪个linux版本,Houdini18.5安装系统环境要求
  14. Gym 101778G
  15. css: 照片有如层叠效果
  16. MySQL之索引及其背后的数据结构
  17. Yii Criteria
  18. 全局gin对象如何在子模块中修改Logger
  19. 1-6月中国ADAS供应商占比9% 又一家零部件巨头全面布局智驾新赛道
  20. ABP vNext 实现租户Id自动赋值插入

热门文章

  1. JAVA端收集Liunx服务器 CPU 内存 磁盘使用率
  2. Unity3d版数字地球、谷歌地球(google earth)
  3. 计算广告——搜索广告技术初窥
  4. OpenVINO系列19_face_detection检测人脸并做标记
  5. 制作二维码过程的详解(基于python)_基于opencv+python的二维码识别
  6. (什么是 RPC?) AND(什么是域名?)AND (DMA是什么?)AND(IRQ是什么?) 选择自 ycool1984 的 Blog
  7. 微信扫一扫二维码直接打开外部浏览器下载app怎么解决
  8. 《现代密码学》学习笔记——第七章 密钥管理[一]
  9. [精简]快速认识钢琴键盘
  10. Centos 搭建DHCP服务,新建独立网卡完成DHCP实验