一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情
☀️ 写在前面
节流和防抖可以归属为前端开发中性能优化的一个节点,同时也是面试中一个经常存在的问题,笔者在几次面试中都经历了被问到过,谈谈你对节流和防抖的看法或者手写一个防抖和节流函数,所以在面经中也经常出现,成为八股文的热门,本质上都是优化高频率执行代码的一种手段。
那么为什么它这么需要被提问呢?让我们先来看看几个日常应用场景:
- 提交表单的时候多按了一次提交按钮,导致表单提交了俩次
- input联想搜索模糊查询的时候,用户在input框不断输入值,导致每次输入一个字符都会查询一次
- 不断的调整浏览器窗口大小会不断触发resize事件
- 滚动一直监听是否到底触发函数
以上这些情况,我们应该如何解决呢?仔细看我们会发现这些场景,都是多次触发的场景,但是多次触发并不是我们要的最终目的,我们的最终目的是想让多次触发变成一次触发,这时候就是要用到节流防抖了。
☀️什么是节流和防抖
以下是正常执行和函数防抖、函数节流的区别,可以把正常执行当做是点击的动作,防抖是当点击停止的时候执行,节流是在一直点击的情况下,间隔时间执行
电梯的比喻: 我们每天坐电梯的时候,先进来一个人,电梯门设置倒数10秒关门,如果只进来一个人,十秒之后直接关闭然后运输,如果是防抖比喻,则是如果再进来一个人,那么电梯会再重新倒数十秒然后关门,如果是节流比喻,那么电梯门会从第一个人进来的时候开始计时,每十秒关一次门,不管之后时候进来多少人,
☀️节流
可以简单的说是重复执行多次,每次按时间间隔执行,代码如下,使用定时器的写法,每次执行都判断是否正在计时,如果没有计时,则开始计时,如果已经计时,就什么也不做,等待定时器执行结束再重新计时执
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay);
}
}
}
🔍来一个小栗子
let testId = document.getElementById('testId')
let val = document.getElementById('testId').value
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay);
}
}
}
let debounceAjax = throttled2(ajax, 500)
testId.addEventListener('keyup',function (e){
debounceAjax(e.target.value)
})
function ajax(text){
console.log(text)
}
☀️防抖
可以简单的说是重复执行,只执行最后一次,代码如下,执行一次则重新计时一次,相当于电梯进来一个人要重新计时关门
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
🔍来写一个小实例(这个实例就可以解决我们开头提出的问题,‘用户在input框不断输入值,导致每次输入一个字符都会查询一次’,这样只有当用户停止输入的时候,才会执行ajax的查询函数)
let testId = document.getElementById('testId')
let val = document.getElementById('testId').value
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
let debounceAjax = debounce(ajax, 500)
testId.addEventListener('keyup',function (e){
debounceAjax(e.target.value)
})
function ajax(text){
console.log(text)
}
☀️小马同学的问题
写到这里MC小马同学可能就会提问了,为什么一定要用apply的写法去调用函数呢,我偏要直接用fn()去执行也没什么区别呀!我真厉害!现在我们来解答一下MC小马同学的问题
<style>
#content{
height: 200px;
width: 200px;
background: lightblue;
}
</style>
<body>
<div id="content">
</div>
</body>
</html>
<script>
function changeNum(e){
console.log(this)
this.innerHTML = e.offsetX
}
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
//fn(args)
//fn.apply(this, args)
timer = null
}, delay);
}
}
}
let bod = document.getElementById('content')
bod.onmousemove = throttled2(changeNum)
如果我们用fn(args)这种方式去执行函数的话,则会造成执行的this指向不明确的情况,这样的changeNum指向的this是全局的window,这样的话innerHtml的值也没法加上去
如果用了apply呢, 效果就不一样了,this的指向直接到div上面,同样的e的值也能拿得到,那么innerHtml的数字也就会跟着改变了
所以小马同学应该去好好复习一下this的指向的功课才行
☀️问题总结
函数用到apply可以将this的指向指到调用的函数上面,把函数当成一个内部函数来执行,如果直接调用的话,则是把函数当成一个全局函数调用,全局函数调用的this指向window,而且没有e的参数传进来,这样会造成e是undefinde,apply可以理解为是把fn的内容拷贝到当前位置进行执行,如下:
timer = setTimeout(() => {
// fn.apply(this, args)//等同于下列写法
this.innerHTML = args[0].offsetX
timer = null
}, delay);