《JavaScript娓娓道来》系列文章属于JavaScript进阶知识,不同于《JavaScript面试大师》系列知识点+刷题的模式,该系列采取:实例+原理+代码的模式来展现写代码的思路,介绍JavaScript进阶过程中的难点,帮助初级工程师成长为中级和高级工程师。
实例:模糊搜索输入框中对于关键字的检索。若每次keyup事件发生都向服务器发送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;
}
具体闭包怎么回事推荐一篇博文前端基础进阶(四):详细图解作用域链与闭包,思路很清晰。
效果如下图:
这时候的搜索显示逻辑变成了:最后一次键盘输入——>等待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;
}
实现的效果如下:
这个时候我们的防抖函数就大功告成啦:)
可以理直气壮跟面试官侃了~