教程[12] JS 的防抖与节流 (一段时间内少执行事件)

2020/Aug/4 我把节流和防抖概念认错了 现在已更正。

开篇废话

这篇文章还是对 ugoira.huggy.moe 做优化的时候顺便写的
上一篇文章为 《一个在可视界面内加载前端的实例》
在上一篇文章提到了这句

// 如果是监听滚动事件,那么优化要做好来,我直接监听的话滚动就开始卡了(因为滚动一下就会 update 一下)

正好今天看到了关于节流与防抖的概念,正好学习了一波然后写了这篇文章 ~~结果发现

分析

节流和防抖 思路都是类似的,都是在一段时间内少真正执行点事件。
区别在于,节流是在一个事件内隔x毫秒执行一次,而防抖是只执行一次

防抖 (debounce)

防抖的意思大概就是节省无意义的流量(只在开始或者结束执行),比如在一个搜索里面的联想,正在输入中的数据就不需要 post 到服务端进行搜索了,浪费流量与资源 当然 想分析用户曾经输入过什么当我没说

比如下面的视频例子用户点击登录按钮后,在转圈圈的期间登录按钮不可点了,等待服务器返回结果后恢复正常

另外一个例子:在 Vue 的文档里面的搜索,很明显在我输入完 model 完以后过了一会,才弹出搜索结果,这样就改善了用户体验 + 服务端压力

节流(throttle)

这个比较用于浏览器的 scroll mousemove resize 之类的事件中。
用户实际上 拖动(resize)/滚动(scroll)/移动鼠标(mousemove)是一个过程,而不是立刻就更改成相关数值,所以浏览器会一直把相关事件传给监听函数中,如果函数逻辑写得复杂点又或者带请求(也就是会耗点时)来不及跟上 DOM 更新 或者打爆后端,所以需要有节流的思想,隔一段事件再真正执行相关事件,减少点渲染(后端)压力

实践

我们先把 document.addEventListener('scroll', scrollevent) 加回来,看看有什么数据可以让我们判断防抖/节流的

另外提示 window.scrollY 在触屏设备上是返回浮点数的,先把数值砍成 int 类型好点
surface / mac

在这个图里:

  • 左边为 触摸(windows)引起的 scroll 事件 右边是鼠标(macOS)滚动引起的 scroll 事件
  • 浏览器已经会尝试少点执行 scroll 事件了,没有动一下就执行一次事件, scroll 事件间隔大概在 10-20ms 之间

动手实践:
html:(足够长能滚动就行)

body {
    padding-top: 2333px;
}

js:

let lastScrollTime = 0
document.addEventListener('scroll',()=>{
    console.log(window.scrollY, +new Date() - lastScrollTime)
    lastScrollTime = +new Date()
})

在了解浏览器会在什么时候传给我们什么数据,以及我们能拿什么数据来进行防抖判断后,我们才能正式开始实践。

实践防抖 (debounce)

大概思路就是,判断函数执行的时间差,如果时间差大于x毫秒,那么就真正执行相关函数(这里的 hit throttling就是),然后将上次执行的时间赋值一下,等待下一个x毫秒再执行

判断时间法

let lastScrollTime = 0
document.addEventListener('scroll', () => {
    let currentScrollTime = +new Date()
    let offset = currentScrollTime - lastScrollTime
    if (currentScrollTime - lastScrollTime > 300) {
        console.log('hit throttling', window.scrollY, offset)
        lastScrollTime = currentScrollTime
    } else {
        console.log('no throttling', window.scrollY, offset)
    }
})

取消监听法

大概思路就是,在一段时间内只执行第一次的事件,然后把关联到的 event 清空,等待x秒后再 listen 回去
动手实践:

const scrollEventTimerF = ()=>{
    console.log('trigger',new Date())
    document.removeEventListener('scroll',scrollEventTimerF)
    setTimeout(() => {
        document.addEventListener('scroll',scrollEventTimerF)        
    }, 2000)
}
document.addEventListener('scroll',scrollEventTimerF)

实践节流 (throttle)

延迟法

我们可以使用 setTimeout 在用户结束滚动后 x毫秒后执行真正要操作的函数
以滚动函数触发的时间的间隔来判断是否需要滚动(触发延迟为固定的 x毫秒)
(感觉一般业务设置 100ms / 500ms 足够了,前面也说了浏览器喂给我们的 scroll 事件一般在 20ms)

实践一下:

// 记录上次滚动事件触发的时间
let lastScrollTime = 0
const scrollEventTimerF = ()=>{
    let currentScrollTime = +new Date()
    lastScrollTime = currentScrollTime
    // 延迟 300 ms
    setTimeout(() => {
        // 相同代表是最后滚动的事件,那么判断就生效,执行业务代码
        if(lastScrollTime == currentScrollTime){
            let Y = window.scrollY.toString().split('.')[0]
            // let Y = Math.floor(window.scrollY)
            console.log(Y,new Date())
        }
    }, 300)
}
document.addEventListener('scroll',scrollEventTimerF)

settimeout-console

这样虽然还是要被 20ms 内触发一次滚动函数 +new Date() 还是要被疯狂执行
不过已经可以避免后面可能的操作 Dom 且无效的操作(没滚动完就渲染页面跟没渲染一样),这样也算优化了
// 其它办法好像没想到了,因为好像怎么样都要延迟一下,只能说是把 lastScrollTime 给换成 lastY(当前页面位置)
此部分代码实例可以在 github/ugoira.huggy.moe 找到

如图所示,三次事件触发间隙在 2 秒以上
add/removeEventListener

定义状态法

我们定义个状态标记下事件是否执行完毕
这是我的 ugoira.huggy.moe 的例子
webStatus
如果 webStatus (当前状态) 包含 d, c, p 那么就不执行这个转换函数了。

总结

在写节流和防抖逻辑的时候,就是首先要想想有什么数据/事件我能够判断的,然后哪些可以节省的,利用这些给现有代码多加几个判断还有 setTimeout 就完事了。

另外对于防抖与节流其实有更好的代码封装,与原理演示,可以参考这篇文章:

当然,业务逻辑要是只需要根据延迟的话,直接拿现成的库也不错:

最后:代码写的不好,有不严谨的多多包含