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


Posted in Javascript onNovember 20, 2020

场景

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

为什么要用防抖和节流?

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

演进

初始实现

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

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 防抖和节流的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
javascript与asp.net(c#)互相调用方法
Dec 13 Javascript
javascript中文本框中输入法切换的问题
Dec 10 Javascript
基于javascript的JSON格式页面展示美化方法
Jul 02 Javascript
Javascript实现飞动广告效果的方法
May 25 Javascript
JS实现带鼠标效果的头像及文章列表代码
Sep 27 Javascript
Ajax分页插件Pagination从前台jQuery到后端java总结
Jul 22 Javascript
微信小程序 视图层(xx.xml)和逻辑层(xx.js)详细介绍
Oct 13 Javascript
jQuery事件详解
Feb 23 Javascript
详解如何创建并发布一个 vue 组件
Nov 08 Javascript
angular4自定义表单控件[(ngModel)]的实现
Nov 23 Javascript
Vue2.0使用嵌套路由实现页面内容切换/公用一级菜单控制页面内容切换(推荐)
May 08 Javascript
vue实现简单瀑布流布局
May 28 Javascript
JavaScript 异步时序问题
Nov 20 #Javascript
JavaScript实现音乐导航效果
Nov 19 #Javascript
JavaScript实现无限轮播效果
Nov 19 #Javascript
微信小程序实现分页加载效果
Nov 19 #Javascript
vue-drawer-layout实现手势滑出菜单栏
Nov 19 #Vue.js
H5 js点击按钮复制文本到粘贴板
Nov 19 #Javascript
JS数据类型分类及常用判断方法
Nov 19 #Javascript
You might like
PHP 如何利用phpexcel导入数据库
2013/08/24 PHP
Smarty中的注释和截断功能介绍
2015/04/09 PHP
PHP基于双向链表与排序操作实现的会员排名功能示例
2017/12/26 PHP
PHP使用星号替代用户名手机和邮箱的实现代码
2018/02/07 PHP
微信支付之JSAPI公众号支付详解
2019/05/15 PHP
Jquery创建一个层当鼠标移动到层上面不消失效果
2013/12/12 Javascript
JavaScript中实现最高效的数组乱序方法
2014/10/11 Javascript
jQuery提示效果代码分享
2014/11/20 Javascript
多个jQuery版本共存的处理方案
2015/03/17 Javascript
有关json_decode乱码及NULL的问题
2015/10/13 Javascript
全面了解addEventListener和on的区别
2016/07/14 Javascript
详解JavaScript时间处理之几个月前或几个月后的指定日期
2016/12/21 Javascript
总结几道关于Node.js的面试问题
2017/01/11 Javascript
JavaScript使用readAsDataUrl方法预览图片
2017/05/10 Javascript
解析Vue 2.5的Diff算法
2017/11/28 Javascript
Vue中使用ElementUI使用第三方图标库iconfont的示例
2018/10/11 Javascript
Vue+axios+WebApi+NPOI导出Excel文件实例方法
2019/06/05 Javascript
WEB前端性能优化的7大手段详解
2020/02/04 Javascript
[46:50]Liquid vs Mineski 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
Python中的urllib模块使用详解
2015/07/07 Python
python调用其他文件函数或类的示例
2019/07/16 Python
Python正则表达式学习小例子
2020/03/03 Python
tensorflow 动态获取 BatchSzie 的大小实例
2020/06/30 Python
基于Python爬取搜狐证券股票过程解析
2020/11/18 Python
阿联酋优惠券服务:Living Kool
2019/12/12 全球购物
EJB与JAVA BEAN的区别
2016/08/29 面试题
致200米运动员广播稿
2014/02/06 职场文书
趣味运动会活动方案
2014/02/12 职场文书
大学生2014全国两会学习心得体会
2014/03/10 职场文书
绩效工资实施方案
2014/03/15 职场文书
大学新闻系求职信
2014/06/03 职场文书
销售口号霸气押韵
2015/12/24 职场文书
银行工作心得体会范文
2016/01/23 职场文书
2019年教师节祝福语精选,给老师送上真诚的祝福
2019/09/09 职场文书
MySQL之PXC集群搭建的方法步骤
2021/05/25 MySQL
小程序实现筛子抽奖
2021/05/26 Javascript