JavaScript 防抖和节流遇见的奇怪问题及解决

场景

网络上已经存在了大量的有关 防抖 和 节流 的文章,为何吾辈还要再写一篇呢?事实上,防抖和节流,吾辈在使用中发现了一些奇怪的问题,并经过了数次的修改,这里主要分享一下吾辈遇到的问题以及是如何解决的。

为什么要用防抖和节流?

因为某些函数触发/调用的频率过快,吾辈需要手动去限制其执行的频率。例如常见的监听滚动条的事件,如果没有防抖处理的话,并且,每次函数执行花费的时间超过了触发的间隔时间的话 – 页面就会卡顿。

演进

初始实现

我们先实现一个简单的去抖函数

function debounce(delay, action) {
 let tId
 return function(...args) {
  if (tId) clearTimeout(tId)
  tId = setTimeout(() => {
   action(...args)
  }, delay)
 }
}

测试一下

// 使用 Promise 简单封装 setTimeout,下同
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
;(async () => {
 let num = 0
 const add = () => ++num

 add()
 add()
 console.log(num) // 2

 const fn = debounce(10, add)
 fn()
 fn()
 console.log(num) // 2
 await wait(20)
 console.log(num) // 3
})()

好了,看来基本的效果是实现了的。包装过的函数 fn 调用了两次,却并没有立刻执行,而是等待时间间隔过去之后才最终执行了一次。

this 怎么办?

然而,上面的实现有一个致命的问题,没有处理 this!当你用在原生的事件处理时或许还不觉得,然而,当你使用了 ES6 class 这类对 this 敏感的代码时,就一定会遇到 this 带来的问题。

例如下面使用 class 来声明一个计数器

class Counter {
 constructor() {
  this.i = 0
 }
 add() {
  this.i++
 }
}

我们可能想在 constructor 中添加新的属性 fn

class Counter {
 constructor() {
  this.i = 0
  this.fn = debounce(10, this.add)
 }
 add() {
  this.i++
 }
}

但很遗憾,这里的 this 绑定是有问题的,执行以下代码试试看

const counter = new Counter()
counter.fn() // Cannot read property 'i' of undefined

会抛出异常 Cannot read property 'i' of undefined,究其原因就是 this 没有绑定,我们可以手动绑定 this .bind(this)

class Counter {
 constructor() {
  this.i = 0
  this.fn = debounce(10, this.add.bind(this))
 }
 add() {
  this.i++
 }
}

但更好的方式是修改 debounce,使其能够自动绑定 this

function debounce(delay, action) {
 let tId
 return function(...args) {
  if (tId) clearTimeout(tId)
  tId = setTimeout(() => {
   action.apply(this, args)
  }, delay)
 }
}

然后,代码将如同预期的运行

;(async () => {
 class Counter {
  constructor() {
   this.i = 0
   this.fn = debounce(10, this.add)
  }
  add() {
   this.i++
  }
 }

 const counter = new Counter()
 counter.add()
 counter.add()
 console.log(counter.i) // 2

 counter.fn()
 counter.fn()
 console.log(counter.i) // 2
 await wait(20)
 console.log(counter.i) // 3
})()

返回值呢?

不知道你有没有发现,现在使用 debounce 包装的函数都没有返回值,是完全只有副作用的函数。然而,吾辈还是遇到了需要返回值的场景。
例如:输入停止后,使用 Ajax 请求后台数据判断是否已存在相同的数据。

修改 debounce 成会缓存上一次执行结果并且有初始结果参数的实现

function debounce(delay, action, init = undefined) {
 let flag
 let result = init
 return function(...args) {
  if (flag) clearTimeout(flag)
  flag = setTimeout(() => {
   result = action.apply(this, args)
  }, delay)
  return result
 }
}

调用代码变成了

;(async () => {
 class Counter {
  constructor() {
   this.i = 0
   this.fn = debounce(10, this.add, 0)
  }
  add() {
   return ++this.i
  }
 }

 const counter = new Counter()

 console.log(counter.add()) // 1
 console.log(counter.add()) // 2

 console.log(counter.fn()) // 0
 console.log(counter.fn()) // 0
 await wait(20)
 console.log(counter.fn()) // 3
})()

看起来很完美?然而,没有考虑到异步函数是个大失败!

尝试以下测试代码

;(async () => {
 const get = async i => i

 console.log(await get(1))
 console.log(await get(2))
 const fn = debounce(10, get, 0)
 fn(3).then(i => console.log(i)) // fn(...).then is not a function
 fn(4).then(i => console.log(i))
 await wait(20)
 fn(5).then(i => console.log(i))
})()

会抛出异常 fn(...).then is not a function,因为我们包装过后的函数是同步的,第一次返回的值并不是 Promise 类型。

除非我们修改默认值

;(async () => {
 const get = async i => i

 console.log(await get(1))
 console.log(await get(2))
 // 注意,修改默认值为 Promise
 const fn = debounce(10, get, new Promise(resolve => resolve(0)))
 fn(3).then(i => console.log(i)) // 0
 fn(4).then(i => console.log(i)) // 0
 await wait(20)
 fn(5).then(i => console.log(i)) // 4
})()

支持有返回值的异步函数

支持异步有两种思路

  1. 将异步函数包装为同步函数
  2. 将包装后的函数异步化

第一种思路实现

function debounce(delay, action, init = undefined) {
 let flag
 let result = init
 return function(...args) {
  if (flag) clearTimeout(flag)
  flag = setTimeout(() => {
   const temp = action.apply(this, args)
   if (temp instanceof Promise) {
    temp.then(res => (result = res))
   } else {
    result = temp
   }
  }, delay)
  return result
 }
}

调用方式和同步函数完全一样,当然,是支持异步函数的

;(async () => {
 const get = async i => i

 console.log(await get(1))
 console.log(await get(2))
 // 注意,修改默认值为 Promise
 const fn = debounce(10, get, 0)
 console.log(fn(3)) // 0
 console.log(fn(4)) // 0
 await wait(20)
 console.log(fn(5)) // 4
})()

第二种思路实现

const debounce = (delay, action, init = undefined) => {
 let flag
 let result = init
 return function(...args) {
  return new Promise(resolve => {
   if (flag) clearTimeout(flag)
   flag = setTimeout(() => {
    result = action.apply(this, args)
    resolve(result)
   }, delay)
   setTimeout(() => {
    resolve(result)
   }, delay)
  })
 }
}

调用方式支持异步的方式

;(async () => {
 const get = async i => i

 console.log(await get(1))
 console.log(await get(2))
 // 注意,修改默认值为 Promise
 const fn = debounce(10, get, 0)
 fn(3).then(i => console.log(i)) // 0
 fn(4).then(i => console.log(i)) // 4
 await wait(20)
 fn(5).then(i => console.log(i)) // 5
})()

可以看到,第一种思路带来的问题是返回值永远会是 旧的 返回值,第二种思路主要问题是将同步函数也给包装成了异步。利弊权衡之下,吾辈觉得第二种思路更加正确一些,毕竟使用场景本身不太可能必须是同步的操作。而且,原本 setTimeout 也是异步的,只是不需要返回值的时候并未意识到这点。

避免原函数信息丢失

后来,有人提出了一个问题,如果函数上面携带其他信息,例如类似于 jQuery 的 $,既是一个函数,但也同时含有其他属性,如果使用 debounce 就找不到了呀

一开始吾辈立刻想到了复制函数上面的所有可遍历属性,然后想起了 ES6 的 Proxy 特性 – 这实在是太魔法了。使用 Proxy 解决这个问题将异常的简单 – 因为除了调用函数,其他的一切操作仍然指向原函数!

const debounce = (delay, action, init = undefined) => {
 let flag
 let result = init
 return new Proxy(action, {
  apply(target, thisArg, args) {
   return new Promise(resolve => {
    if (flag) clearTimeout(flag)
    flag = setTimeout(() => {
     resolve((result = Reflect.apply(target, thisArg, args)))
    }, delay)
    setTimeout(() => {
     resolve(result)
    }, delay)
   })
  },
 })
}

测试一下

;(async () => {
 const get = async i => i
 get.rx = 'rx'

 console.log(get.rx) // rx
 const fn = debounce(10, get, 0)
 console.log(fn.rx) // rx
})()

实现节流

以这种思路实现一个节流函数 throttle

/**
 * 函数节流
 * 节流 (throttle) 让一个函数不要执行的太频繁,减少执行过快的调用,叫节流
 * 类似于上面而又不同于上面的函数去抖, 包装后函数在上一次操作执行过去了最小间隔时间后会直接执行, 否则会忽略该次操作
 * 与上面函数去抖的明显区别在连续操作时会按照最小间隔时间循环执行操作, 而非仅执行最后一次操作
 * 注: 该函数第一次调用一定会执行,不需要担心第一次拿不到缓存值,后面的连续调用都会拿到上一次的缓存值
 * 注: 返回函数结果的高阶函数需要使用 {@link Proxy} 实现,以避免原函数原型链上的信息丢失
 *
 * @param {Number} delay 最小间隔时间,单位为 ms
 * @param {Function} action 真正需要执行的操作
 * @return {Function} 包装后有节流功能的函数。该函数是异步的,与需要包装的函数 {@link action} 是否异步没有太大关联
 */
const throttle = (delay, action) => {
 let last = 0
 let result
 return new Proxy(action, {
  apply(target, thisArg, args) {
   return new Promise(resolve => {
    const curr = Date.now()
    if (curr - last > delay) {
     result = Reflect.apply(target, thisArg, args)
     last = curr
     resolve(result)
     return
    }
    resolve(result)
   })
  },
 })
}

总结

嘛,实际上这里的防抖和节流仍然是简单的实现,其他的像 取消防抖/强制刷新缓存 等功能尚未实现。当然,对于吾辈而言功能已然足够了,也被放到了公共的函数库 rx-util 中。

以上就是JavaScript 防抖和节流遇见的奇怪问题及解决的详细内容,更多关于JavaScript 防抖和节流的资料请关注我们其它相关文章!

(0)

相关推荐

  • js防抖函数和节流函数使用场景和实现区别示例分析

    本文实例讲述了js防抖函数和节流函数使用场景和实现区别.分享给大家供大家参考,具体如下: 开发过程中,都遇到过某个事件被频发触发的场景,比如resize,scroll事件,input事件,而对应的事件处理函数也会被高频率调用,这时会增加浏览器负担,用户体验也不好,这也是防抖函数和节流函数存在的意义和使用场景. 函数防抖(debounce): 持续触发事件时,在设定时间段内没有被触发,才去调用事件处理函数,在设定时间段内如果事件又被触发,则不调用事件处理函数,并从触发事件时间重新开始延时. 具体实

  • 如何在面试中手写出javascript节流和防抖函数

    面试的时候我们经常会问别人是理解什么是节流和防抖,严格的可能要求你写出节流和防抖函数,这里我们抛开loadsh工具库手写节流和防抖 1.节流函数throttle // 节流方案1,每delay的时间执行一次,通过开关控制 function throttle(fn, delay, ctx) { let isAvail = true return function () { let args = arguments // 开关打开时,执行任务 if (isAvail) { fn.apply(ctx,

  • JS中的防抖与节流及作用详解

    概念 函数防抖(debounce)是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次,比如说用手指一直按住一个弹簧,它将不会弹起直到你松手为止. 函数节流是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是如果将水龙头拧紧直到水是以水滴的形式流出

  • js节流防抖应用场景,以及在vue中节流防抖的具体实现操作

    故事背景: 项目有个需求是输入框在输入的时候进行搜索,展示下拉数据,但是没必要输入一个字都进行搜索,所以想到了在输入结束200毫秒后再进行搜索,从而引出来了 js的节流(throttle),防抖(debounce),在网上想找个现成的用下,但是好多都不对,于是就自己搞了. 先看看概念 函数防抖(debounce): 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时:典型的案例就是输入搜索:输入结束后n秒才进行搜索请求,n秒内又输入的内容,就重新计时. 函数节流(throttle)

  • JS函数节流和函数防抖问题分析

    问题1:如果实现了dom拖拽功能,但是在绑定拖拽事件的时候发现每当元素稍微移动一点便触发了大量的回调函数,导致浏览器直接卡死,这个时候怎么办? **问题2:**如果给一个按钮绑定了表单提交的post事件,但是用户有些时候在网络情况极差的情况下多次点击按钮造成表单重复提交,如何防止多次提交的发生? 为了应对如上场景,便出现了 函数防抖 和 函数节流 两个概念,总的来说: 这两个方法是在 时间轴上控制函数的执行次数. 函数防抖(debounce) 概念: 在事件被触发n秒后再执行回调,如果在这n秒内

  • js防抖和节流的深入讲解

    前言: 我们在做页面事件绑定的时候,经常要进行节流处理,比如鼠标异步点击,去执行一个异步请求时,需要让它在上一次没执行时不能再点击,又或者绑定滚动事件,这种持续触发进行dom判断的时候,就要按一定频率的执行. 本文主要给大家介绍了关于js防抖和节流的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 0. 引入 首先举一个例子: 模拟在输入框输入后做ajax查询请求,没有加入防抖和节流的效果,这里附上完整可执行代码: <!DOCTYPE html> <html la

  • JS防抖和节流实例解析

    日常开发过程中,滚动事件做复杂计算频繁调用回调函数很可能会造成页面的卡顿,这时候我们更希望把多次计算合并成一次,只操作一个精确点,JS把这种方式称为debounce(防抖)和throttle(节流) 函数防抖 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定时间到来之前,又触发了事件,就重新开始延时.也就是说当一个用户一直触发这个函数,且每次触发函数的间隔小于既定时间,那么防抖的情况下只会执行一次. function debounce(fn, wait) { va

  • JavaScript函数节流和函数防抖之间的区别

    一.概念解释 函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段. 大家大概都知道旧款电视机的工作原理,就是一行行得扫描出色彩到屏幕上,然后组成一张张图片.由于肉眼只能分辨出一定频率的变化,当高频率的扫描,人类是感觉不出来的.反而形成一种视觉效果,就是一张图.就像高速旋转的风扇,你看不到扇叶,只看到了一个圆一样. 同理,可以类推到js代码.在一定时间内,代码执行的次数不一定要非常多.达到一定频率就足够了.因为跑得越多,带来的效果也是一样.倒不如,把js代码的执行次数控制在合理的范围.既

  • JS函数节流和防抖之间的区分和实现详解

    在写JS时,这两个函数比较常见,有时候傻傻分不清用哪个,或者说知道代码要怎么写,但要说出它究竟是节流函数还是防抖函数时一脸楞逼.今天有一个同学分享了这两个的区分,我也来回顾一下,加深一下印象,以便日后用到时心里有底.PS:百度和谷歌搜索前几个介绍都是相反介绍,本文为原创,如有雷同纯属抄袭我的. 节流概念(Throttle) 按照设定的时间固定执行一次函数,比如200ms一次.注意:固定就是你在mousemove过程中,执行这个节流函数,它一定是200ms(你设定的定时器延迟时间)内执行一次.没到

  • 深入了解JavaScript 防抖和节流

    概述 说明 在项目过程中,经常会遇到一个按钮被多次点击并且多次调用对应处理函数的问题,而往往我们只需去调用一次处理函数即可.有时也会遇到需要在某一规则内有规律的去触发对应的处理函数,所以就需要使用到函数防抖与函数节流来帮助我们实现我们想要的结果以及避免不必要的问题产生. 函数防抖(debounce) 定义:当持续触发事件时(如连续点击按钮多此),一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,有一次触发了事件,就重新开始延时. 原理:维护一个计时器,规定在延时时间后

随机推荐