在进入学习之前,先想一件事情:从哪听说的这俩名词?
我想大部分人应该是在编程中遇到问题后听他人说的。我希望看完这篇博客后能帮你解决实际问题,不只玩玩而已。
简单来说,防抖与节流是为了优化性能,提升用户体验的。
实现原理都是延迟执行,减少调用次数。
不同的是,对于一段时间内的频繁调用,防抖是延迟执行后一次调用,节流是延迟定时多次调用。
使用场景:一般为存在用户交互,需要监听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 }
希望在这里有所收获 ~