防抖和节流

在进入学习之前,先想一件事情:从哪听说的这俩名词?

我想大部分人应该是在编程中遇到问题后听他人说的。我希望看完这篇博客后能帮你解决实际问题,不只玩玩而已。

简单来说,防抖节流是为了优化性能提升用户体验的。

实现原理都是延迟执行减少调用次数

不同的是,对于一段时间内的频繁调用,防抖延迟执行后一次调用节流延迟定时多次调用

使用场景:一般为存在用户交互,需要监听DOM元素时。

下面我们模拟场景,带你学透防抖节流

防抖


模拟场景:一个搜索输入框,在输入后立即(实时)展现搜索结果,不用点击按钮。

实现方法:

1. 最基本的实现方式是绑定 input 元素的键盘事件,然后在监听函数中发送 AJAX 请求。

const ipt = document.querySelector('input')
ipt.addEventListener('input', e => {
  search(e.target.value).then(resp => {
    // ...
  }, e => {
    // ...
  })
})

2. 使用UI框架的话(比如antd)可以利用其 onChange 函数,实时发送 AJAX 请求。

<Input 
  placeholder = "Please input!"
  onChange = {(e)=>{console.log('input value', e)}}
></Input>

缺点:性能不好,用户每次输入都会发送请求,而可能只有最后一次搜索的结果才是用户想要的,所以只有最后一次的搜索是有必要的,前面进行的查询都是无意义的浪费网络带宽和服务器资源


解决方法:为这类连续触发的事件,添加防抖功能 → 为函数的执行设置一个合理的时间间隔,避免事件在时间间隔内频繁触发,同时又保证用户输入后能即时看到搜索结果。

(以下都不考虑框架,用纯 js 写法。)

1.  setTimeout() 让函数延迟执行。

const ipt = document.querySelector('input')
let timeout = null
ipt.addEventListener('input', e => {
  // 判断 timeout 实例是否存在,如果存在则销毁,然后创建一个新的定时器,防止累计定时。
  if(timeout) {
    clearTimeout(timeout)
    timeout = null
  }
  timeout = setTimeout(() => {
    search(e.target.value).then(resp => {
      // ...
    }, e => {
      // ...
    })
  }, 500)
})

问题虽然解决了,但这并不是最优解。如果我们需要参数和返回值怎么办?如果用这个功能的地方多,每个地都要写一次吗?防抖功能是不是可以手动控制执行和取消? 

2. 抽离成公共组件,原函数作为参数传入 debounce() 函数中,同时指定延迟等待时间,返回一个新的函数,这个函数包含 cancel 属性,用来取消原函数执行。flush 属性用来立即调用原函数,同时将原函数的执行结果以 Promise 的形式返回。

/** 
 * 入参:
 *    func 要防抖动的函数
 *    wait 需要延迟的毫秒数
 * 返回值:
 *    debounced:防抖动功能的函数
 */
const debounce = (func, wait = 0) => {
  let timeout = null
  let args
  // 主要起作用的函数
  function debounced(...arg) {
    args = arg
    if(timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函数执行结果
    return new Promise((res, rej) => {
      timeout = setTimeout(async () => {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch(e) {
          rej(e)
        }
      }, wait)
    })
  }
  // 取消
  function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  // 立即执行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

3. 防抖函数的使用 

// 获取dom元素
const ipt = document.querySelector('input');

// 防抖监听
var iptDebounce = debounce(calculateLayout, 150);
ipt.addEventListener('input', iptDebounce);

// 立即执行防抖调用
iptDebounce.flush();

// 取消防抖调用
iptDebounce.cancel();

lodash 的 debounce() 函数有更丰富的功能,点我到 GitHub 上查看

节流


模拟场景:一个左右两列布局的查看文章页面,左侧是大纲结构,右侧是文章内容。现在为其添加一个功能:当用户滚动阅读右侧文章内容时,左侧大纲相对应部分高亮显示,提示用户当前阅读位置。

实现方法:滚动前记录大纲中各个章节的垂直距离,然后监听 scroll 事件的滚动距离,根据距离的比较来判断需要高亮的章节。

// 监听scroll事件
wrap.addEventListener('scroll', e => {
  let highlightId = ''
  // 遍历大纲章节位置,与滚动距离比较,得到当前高亮章节id
  for (let id in offsetMap) {
    if (e.target.scrollTop <= offsetMap[id].offsetTop) {
      highlightId = id
      break
    }
  }
  const lastDom = document.querySelector('.highlight')
  const currentElem = document.querySelector(`a[href="#${highlightId}"]`)
  // 修改高亮样式
  if (lastDom && lastDom.id !== highlightId) {
    lastDom.classList.remove('highlight')
    currentElem.classList.add('highlight')
  } else {
    currentElem.classList.add('highlight')
  }
})

缺点:滚动事件的触发频率非常高,持续调用判断函数很可能会影响渲染性能。实际上也不需要过于频繁地调用,因为如果用户滚动 1 像素,很有可能当前章节的阅读并没有发生变化。

解决办法:添加节流功能 在指定一段时间内只调用一次函数,从而降低函数调用频率。

节流有两种执行方式  在调用函数时,执行最先一次  or 最近一次,所以需要设置时间戳加以判断。

const throttle = (func, wait = 0, execFirstCall) => {
  let timeout = null
  let args
  let firstCallTimestamp

  function throttled(...arg) {
    if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
    if (!execFirstCall || !args) {
      console.log('set args:', arg)
      args = arg
    }
    if (timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函数执行结果
    return new Promise(async(res, rej) => {
      if (new Date().getTime() - firstCallTimestamp >= wait) {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch (e) {
          rej(e)
        } finally {
          cancel()
        }
      } else {
        timeout = setTimeout(async () => {
          try {
            const result = await func.apply(this, args)
            res(result)
          } catch (e) {
            rej(e)
          } finally {
            cancel()
          }
        }, firstCallTimestamp + wait - new Date().getTime())
      }
    })
  }
  // 允许取消
  function cancel() {
    clearTimeout(timeout)
    args = null
    timeout = null
    firstCallTimestamp = null
  }
  // 允许立即执行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  throttled.cancel = cancel
  throttled.flush = flush
  return throttled
}

 

 

希望在这里有所收获 ~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值