JavaScript,冲鸭系列——函数防抖

笔者在这里按照先感性认识,再介绍原理,最后上手操作并且将写代码的思路一步一步都写出来的过程来介绍JavaScript中的一个难点函数防抖。话语可能显得比较啰嗦,但是笔者还是本着授人以渔的方针展示自己的思维过程。目标读者是JavaScript初级开发人员。希望读者有好的建议或者不同的观点可以不吝赐教。

实例:模糊搜索输入框中对于关键字的检索。若每次keyup事件发生都向服务器发送ajax请求会极大浪费资源,造成浏览器卡顿以及服务器的卡顿(如下图所示)。

模拟模糊搜索ajax请求
模拟模糊搜索ajax请求

核心代码如下:

<input type="text" id="input">

<script>
var input = document.getElementById("input");
input.addEventListener("keyup", ajax);

function ajax() {
    console.log('ajax发送的数据为: ' + input.value);
}
</script>

作用:防止短时间内多次触发方法,造成浏览器抖动或卡顿。

原理:当触发某次事件之后一段时间(这里我们设为wait)内,再没有触发事件,那么该次事件回调会被执行。总结来说就是:短时间内无论事件触发多少次,总是只会执行最后一次事件的回调方法。

目标:上述例子中,只有当键盘停止输入才会发送ajax请求。

根据原理我们不难想到将事件的回调函数作为setTimeout的回调函数,设置setTimeout的时间为wait。我们还需要一个全局变量timeout来保存当前所处的计时阶段,如果距离上次事件发生wait时间段之内,那么我们把timeout清除,并重新计时,如果wait期间没有发生再次触发相同事件,那么执行fn方法,也就是ajax方法。

var input = document.getElementById("input");
input.addEventListener("keyup", debounce(ajax, 1000));

function ajax() {
    console.log('ajax发送的数据为: ' + input.value);
}

// 全局变量timeout用来保存当前所处的计时阶段
var timeout = null;

// 版本1
function debounce(fn, wait) {
    if(timeout) clearTimeout(timeout);

    timeout = setTimeout(function() {
        fn();
    }, 1000)
}

而由于JavaScript垃圾回收的机制知道,全局变量什么时候需要自动释放内存空间则很难判断,因此在开发中,需要尽量避免使用全局变量。这时候可以对上述的代码做一些改进,能不能将timeout作为一个局部变量放在某个函数中,比如说debounce中,然后还可以一直保存在内存中,可以随时改变状态呢?答案是可以的。将变量一直保存在内存中正是闭包的特点。我们使用闭包改进代码。

// 版本2

// 闭包形式1
function debounce(fn) {
    //父作用域debounce的变量timeout被子作用域匿名函数访问,形成闭包
    var timeout = null;

    return function() {
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(function() {
            fn();
        }, 1000)
    }
}

// 闭包形式2
function debounce(fn) {
    var timeout = null;

    var debounced = function() {
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(function() {
            fn();
        }, 1000)
    }
    return debounced;
}

具体闭包怎么回事推荐一篇博文前端基础进阶(四):详细图解作用域链与闭包,思路很清晰。

扫描二维码关注公众号,回复: 3952870 查看本文章

效果如下图:

这时候的搜索显示逻辑变成了:最后一次键盘输入——>等待wait时间段——>执行ajax方法,也就是说我们在实际的搜索过程中,只有当停止输入且过了一段时间之后才能看到搜索框的显示内容,这显然是用户不友好的。我们需要改进代码,变成:最后一次键盘输入之后立即执行ajax方法,然后等待wait时间段,而不是上图的逻辑。这实际上是代码执行顺序的改变,代码改进后如下图所示。

// 版本3

function debounce(fn, wait) {
    var timeout = null;

    var debounced = function() {
        // callNow用来保存当事件触发一瞬间前的计时状态
    	var callNow = !timeout;

        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(function(){
        	timeout = null
        }, wait)
        if(callNow){
        	fn();
        };
    }
    return debounced;
}

效果如下图所示:

这时候的搜索显示逻辑变成了:如果此次输入距离上次超过wait,则立即执行ajax,否则重新开始倒计时。

这个时候debounce函数已经到了版本三,我们先来开一个副分支任务,积累一下经验。

《JavaScript高级程序设计》在5.5.4函数内部属性中讲了这么一句:

在函数内部,有两个特殊的对象,arguments和this。

其中arguments是个类数组对象,包含着传入函数的所有参数,而this引用的是函数据以执行的执行上下文。笔者采用了Dom2级的事件处理,与Dom0级一样,事件处理程序在其依附的元素的作用域中运行。因此在执行ajax方法的时候正常情况this将打印出input元素,而arguments[0]将访问到具体的事件。我们将它们打印出来如下图。

function ajax(e) {
	console.log('arguments: ', arguments[0]);
    console.log('ajax发送的数据为: ' + this.value);
}

实际上执行debounce(ajax, 1000)之后,ajax方法在debounced方法中独立调用,arguments实际上是传给了debounced,而且ajax中的this变成了window对象。因此我们需要对把ajax中的arguments和this改正过来。代码如下。

// 版本4

function debounce(fn, wait) {
    var timeout = null;

    var debounced = function() {
        // 用that保存dom2级事件处理中绑定的元素对象
        // arg保存默认传给事件处理程序的参数
    	var that = this,
    		arg = arguments;
    	var callNow = !timeout;

        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(function(){
        	timeout = null
        }, wait)
        if(callNow){
        	fn.apply(that,arg);            
        };
    }
    return debounced;
}

此时arguments和this指向正确的对象。

我们再为debounce函数添加一个immediate参数,用来判断是采用版本二的”停止键盘输入——>等待wait时间段——>执行ajax方法“(immediate为false)还是版本三的“停止键盘输入——>立即执行ajax方法——>等待wait时间段”(immediate为true)的逻辑。

// 版本五

function debounce(fn, wait, immediate) {
    var timeout = null,
        result;

    var debounced = function() {
        var that = this,
            arg = arguments;
        var callNow = !timeout;

        if (immediate) {
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(function() {
                timeout = null;
            }, wait)
            if (callNow) {
                result = fn.apply(that, arg);
            };
        } else {
        	timeout = setTimeout(function(){
        		fn.apply(that, arg);
        	}, wait)
        }

        return result;
    }

    return debounced;
}

此外还能再版本五中我们还增加了返回值,因为ajax方法可能是由返回值的。当immediate为false的非立即执行情况下,由于fn.apply(that, arg)是在setTimeout内部,异步执行的,return result在获得ajax方法返回值之前就执行了,因此只会返回undefined。所以我们只需要在immediate为true的立即执行情况下对result赋值。

这里最后还有这么一个应用场景:某次键盘输入并执行ajax方法之后,我们不想等wait时间才能再次执行ajax方法,而是想又能继续立即执行ajax方法。这种情况下需要我们为debounced添加一个取消方法,而取消方法的原理很简单,首先将timeout定时器从异步队列中删除,然后手动将timeout置为null,版本六代码如下。

// 版本六

function debounce(fn, wait, immediate) {
    var timeout = null,
        result;

    var debounced = function() {
        var that = this,
            arg = arguments;
        var callNow = !timeout;

        if (immediate) {
            if (timeout) clearTimeout(timeout);
            timeout = setTimeout(function() {
                timeout = null;
            }, wait)
            if (callNow) {
                result = fn.apply(that, arg);
            };
        } else {
        	timeout = setTimeout(function(){
        		fn.apply(that, arg);
        	}, wait)
        }

        return result;
    }

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    }
    return debounced;
}

实现的效果如下:

这个时候我们的防抖函数就大功告成啦:)

可以理直气壮跟面试官侃了~

猜你喜欢

转载自blog.csdn.net/wuxeek/article/details/83620403