节流与防抖

防抖

防抖, 仅仅从字面去理解,就是防止抖动,关键点是等待,等待300ms,如果没有新的action,就执行。
这里举一个更形象的例子,也是使用此场景最多的例子。

有一个搜索输入框,为了提升用户体验,希望在用户输入后可以立即展现搜索结果,而不是每次输入完后还要点击搜索按钮。最基本的实现方式应该很容易想到,那就是绑定 input 元素的键盘事件,每次输入的时候,触发input,向后台发起请求。

类似于以下伪代码:

const inputEle = document.querySelector('input')
async function search (e) {
    await getUser({name: e.targe.value})
}
inputEle.addEventListener('input', search)

但是这个时候,后端提出了一个问题,不希望用户每输入一个字符,就发起一次后端请求,这样会造成资源的浪费。例如每当用户输入一个字符,都会触发搜索,而实际上,只有最后一次搜索结果是用户想要的,前面进行了 2 次无效查询,浪费了网络带宽和服务器资源。

  • 1: 'l'
  • 2: 'li'
  • 3: 'liu'

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

接下来,我们分析下如何实现一个简单版的的防抖

function debounce (fn, wait = 0) {
  let timeout = null
  return function () {
    // 如果已经存在定时器,说明是在wait间隔之内触发的,需要重新计时
    if (timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    let self = this
    let args = Array.prototype.slice(arguments)
    timeout = setTimeout(() => {
      fn.call(self, ...args)
    }, wait)
  }
}

如果通过箭头函数,也可以这么写

function debounce = (fn, wait = 0) => {
    let timeout = null
    return (...args) => {
        if (timeout) {
            clearTimeout(timeout)
            timeout = null
        }
        timeout = setTimeout(async () => {
            // 箭头函数里的this,指向定义时的上一层,没有自己的this
            await fn.apply(this, args)
        }, wait)
    }
}

通过这段代码,我们做测试和调试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>截流与防抖</title></title>
    <script src="./debounce.js"></script>
</head>
<body>
    <div>
        <input type="text"> 请输入
    </div>
    <script>
        const inputEle = document.querySelector('input')
        async function search (e) {
            console.log(this)
            console.log('input==>', e.target.value)
        }
        inputEle.addEventListener('input', debounce(search, 200))
    </script>
</body>
</html>

接下来,我们实现一个豪华版的防抖

const debounce = (func, await = 0) => {
  let timeout = null
  let args
  function dobounced(...arg) {
    args = arg
    if (timeout) {
      clearTimeout(timeouts)
      timeout = null
    }
    return new Promise((resolve, reject) => {
      timeout = setTimeout(async() => {
        try {
          const res = await func.apply(this, args)
          resolve(res)
        } catch (e) {
          reject(e)
        }
      }, wait)
    })
  }
  // 允许取消
  function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  // 允许立即执行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

节流

我们知道有个词语,叫做开源节流。这里的节流是省着消费。而我们JS里的节流,也用相同的意义,减少没有必要的dom操作,因为dom操作是昂贵的。节流的关键词是丢弃, 500ms内只执行一次,其余的action 被丢弃。打一个生活中的比喻就是:我脑海里无时无刻都有购物的想法,但是要省钱,就必须节制购物,给自己定下规则,3个月内只能买一次物品。当然现实生活中,小伙伴们不会对自己这么严苛啊,这里的例子只是方便大家理解。

我们举一个项目上的应用,加深对节流的理解。

例子来源于拉勾教育-前端高手进阶, 有兴趣可以到应用里学习
一个左右两列布局的查看文章页面,左侧为文章大纲结构,右侧为文章内容。现在需要添加一个功能,就是当用户滚动阅读右侧文章内容时,左侧大纲相对应部分高亮显示,提示用户当前阅读位置。这个功能的实现思路比较简单,滚动前先记录大纲中各个章节的垂直距离,然后监听 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 像素的时候,很有可能当前章节的阅读并没有发生变化。所以我们可以设置在指定一段时间内只调用一次函数,从而降低函数调用频率,这种方式我们称之为“节流”。

方法一:
通过一个标识位,来实现第一个截流,可以满足一般场景

 function throttle (fn, interval = 0) {
    let isExecute = true
    return function (...args) {
      if (isExecute) {
        fn.apply(this, args)
        isExecute = false
        setTimeout(() => {
          isExecute = true
        }, interval) 
      }
    }
  }

方法二:
通过时间来控制

第一次就会触发, 因为第一次last ===0 ,导致 now 是一定大于 delay的,所以第一次必须触发

function throttle(fn, interval) {
    let last = 0
    return function () {
        let now = Date.now()
        let context = this
        let args = arguments
        
        if (now - last > interval) {
            last = Date.now()
            fn.apply(context, args)
        }
    }
}

方法三,思想类似与方法一

第一次也是延迟执行,但是用户最后一次操作,也会延迟执行

function throttle(fn, interval = 0) {
  let timer = null
  return function () {
      let args = arguments
      if (!timer) {
        timer = setTimeout(() => {
              fn.apply(this, args)
              clearTimeout(timer)
              timer = null
          }, interval)
      }
  }
}

方法四:更精确的时间控制防抖

function throttle5(fn, interval) {
  let timer = null
  let startTime = Date.now()
  return function () {
      let curTime = Date.now()
      let remainning = interval - (curTime - startTime)
      let context = this
      let args = arguments
      clearTimeout(timer)
      // 操作已过剩余时间,立即执行
      if (remainning <=0) {
          // 重新计时
          startTime = Date.now()
          fn.apply(context, args)
      } else {
          timer = setTimeout(() => {
              fn.apply(context, args)
          }, remainning)
      }
  }
}

可以通过下面这点代码来验证调试throttle

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>截流与防抖</title></title>
    <script src="./debounce.js"></script>
    <script src="./throttle.js"></script>
    <style>
        .test {
            width: 200px;
            height: 200px;
            overflow: auto;
            background-color: burlywood;
        }
    </style>
</head>
<body>
    <div class="test">
        一个左右两列布局的查看文章页面,左侧为文章大纲结构,右侧为文章内容。现在需要添加一个功能,就是当用户滚动阅读右侧文章内容时,左侧大纲相对应部分高亮显示,提示用户当前阅读位置。这个功能的实现思路比较简单,滚动前先记录大纲中各个章节的垂直距离,然后监听 scroll 事件的滚动距离,根据距离的比较来判断需要高亮的章节。 
    </div>
    <script>
        const ele = document.querySelector('.test')
        function scrollFn (e) {
            console.log('this==>', this)
            console.log('top', e.target.scrollTop)
        }
        ele.addEventListener('scroll', throttle5(scrollFn, 500))
    </script>
</body>
</html>
posted @ 2021-08-03 19:04  eastsae  阅读(57)  评论(0编辑  收藏  举报