在维护一个老项目时,我们给搜索框接入了jQuery UI Autocomplete,并通过远程接口实现候选词的分页加载。功能在Chrome和Firefox下表现正常,但测试反馈IE9浏览器里滚动下拉列表会连续触发多次相同的AJAX请求,有时页面还会出现卡顿。开始以为是Autocomplete的source回调执行了多次,后来定位到问题出在scroll事件的处理方式和异步请求的时序上。

为了说清楚这个问题,我们先还原一下当时的实现方式。Autocomplete的source函数负责向服务端请求数据,滚动加载则是在下拉菜单出现后监听其scroll事件,当滚动条接近底部时递增页码并重新发起一次搜索。这套逻辑本身并不复杂,但一旦放到IE9里,各种细节问题就会被放大。
一、重复请求的触发链路分析
Autocomplete组件在处理远程数据时,source函数会在用户输入或程序调用search方法时被触发。对于分页场景,常见做法是在source函数外维护一个currentPage变量,初始为1。当用户滚动到底部时,将currentPage加1,然后再次调用Autocomplete的search方法,让source重新执行并携带新的页码。这个流程理论上只会产生一次新的请求,但实际在IE9中却可能产生两到三次甚至更多的重复请求。
其中最关键的触发点在于scroll事件的绑定方式。很多开发者会在autocompleteopen事件中给下拉菜单绑定scroll监听,但没有在autocompleteclose时解绑。Autocomplete每次打开下拉菜单都会重新执行绑定逻辑,导致同一个菜单上累积多个scroll回调。IE9对scroll事件的触发非常敏感,鼠标滚轮轻轻滚动一下,可能会在极短时间内冒出几十个scroll事件,多个回调叠加后就会同时发起多个请求。
下面的代码就是问题出现时的一个典型实现:
$('#search').autocomplete({
source: function(request, response) {
$.ajax({
url: '/api/search',
data: {
term: request.term,
page: currentPage,
pageSize: 20
},
success: function(data) {
response(data.items);
}
});
},
minLength: 1
}).on('autocompleteopen', function() {
$('.ui-autocomplete').on('scroll', function() {
var menu = $(this);
if (menu.scrollTop() + menu.innerHeight() >= menu[0].scrollHeight - 10) {
currentPage++;
$('#search').autocomplete('search', $('#search').val());
}
});
});
这段代码存在几个明显缺陷。其一是scroll事件在autocompleteopen中反复绑定,每次打开下拉菜单都会叠加一个新的回调,滚动一次会触发所有历史回调。其二是currentPage递增没有任何保护,即使已经加载到最后一页也会继续增加并发起无效请求。其三是请求没有做取消处理,前一页的响应可能在后一页之后返回,造成数据顺序错乱。其四是没有防抖,IE9里滚动事件触发频率极高,同一时刻可能有多个请求同时发出。
二、基于请求锁与防抖的优化方案
要解决IE9下的重复请求问题,核心思路是从请求时序和事件频率两方面同时下手。首先需要引入一个isLoading标志作为请求锁,当有请求正在进行时直接忽略新的加载请求。其次要对滚动事件做防抖处理,在用户停止滚动一段时间后再判断是否需要加载下一页。再次要维护总页数totalPages,加载完成前页码递增前必须判断是否还有更多数据。最后还要利用jQuery的abort方法取消尚未完成的旧请求,避免响应顺序错乱。
先看请求锁和取消请求的实现。我们会用一个pendingRequest变量保存当前正在执行的jqXHR对象,在新的请求发起前先abort掉上一个请求。这样即使滚动事件触发了多次,也只会保留最后一次有效请求。同时isLoading在请求开始前被置为true,在success或error回调中恢复为false。请求锁可以防止在响应返回之前重复发起相同页码的请求。
下面是优化后的核心请求函数:
var isLoading = false;
var currentPage = 1;
var totalPages = 1;
var pendingRequest = null;
function loadPage(term, page, append) {
if (isLoading || page > totalPages) {
return;
}
isLoading = true;
if (pendingRequest) {
pendingRequest.abort();
}
pendingRequest = $.ajax({
url: '/api/search',
data: {
term: term,
page: page,
pageSize: 20
},
dataType: 'json',
success: function(data) {
totalPages = data.totalPages;
if (append) {
// 将data.items追加到已有结果中
} else {
// 替换当前结果
}
isLoading = false;
},
error: function() {
isLoading = false;
}
});
}
滚动事件的处理需要放在autocompleteopen中,但必须先解绑再绑定,防止历史回调累积。在回调内部使用setTimeout做防抖,200毫秒内如果再次滚动则清除定时器重新计时。只有滚动位置接近底部且当前页小于总页数时才递增页码并调用loadPage。这样即便IE9频繁触发scroll事件,实际发起请求的次数也会被严格控制。
$('#search').on('autocompleteopen', function() {
var menu = $('.ui-autocomplete');
menu.off('scroll.autocompletePager').on('scroll.autocompletePager', function() {
var scrollTimer = null;
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(function() {
if (menu.scrollTop() + menu.innerHeight() > menu[0].scrollHeight - 12) {
if (currentPage < totalPages) {
currentPage++;
loadPage($('#search').val(), currentPage, true);
}
}
}, 200);
});
});
这里使用命名空间scroll.autocompletePager来绑定事件,这样下次打开时可以精确地移除之前的监听,不会影响其他可能存在的scroll处理逻辑。防抖时间设为200毫秒在IE9下是一个比较平衡的值,既能保证响应速度,又能避免过于频繁的请求。
三、完整集成与数据缓存策略
只解决请求重复还不够,实际项目中还需要考虑数据合并和缓存。当用户滚动加载第二页时,新数据需要追加到Autocomplete的已有结果后面,而不是整体替换。这需要修改source函数,让它在首次加载和分页加载时走不同的数据合并路径。同时可以为每个搜索词维护一个页码缓存,已经请求过的页码不再重复请求。
为了实现缓存,可以定义一个cache对象,以搜索词和页码的组合作为键,存储请求返回的数据。当loadPage被调用时,先检查缓存中是否有对应数据,有则直接使用,没有才发起AJAX请求。成功后将数据写入缓存。当用户修改搜索词时,需要清空旧缓存并重置currentPage和totalPages。这样可以大幅减少服务端压力,尤其是在用户来回滚动时。
下面是一个完整的集成示例,包含source回调、响应合并和缓存处理:
var cache = {};
var isLoading = false;
var currentPage = 1;
var totalPages = 1;
var pendingRequest = null;
var currentTerm = '';
function getCacheKey(term, page) {
return term + '_' + page;
}
function loadPage(term, page, append, callback) {
if (isLoading || page > totalPages) {
return;
}
var cacheKey = getCacheKey(term, page);
if (cache[cacheKey]) {
callback(cache[cacheKey], append);
return;
}
isLoading = true;
if (pendingRequest) {
pendingRequest.abort();
}
pendingRequest = $.ajax({
url: '/api/search',
data: {
term: term,
page: page,
pageSize: 20
},
dataType: 'json',
success: function(data) {
totalPages = data.totalPages;
cache[cacheKey] = data.items;
callback(data.items, append);
isLoading = false;
},
error: function() {
isLoading = false;
}
});
}
$('#search').autocomplete({
source: function(request, response) {
if (request.term !== currentTerm) {
currentTerm = request.term;
currentPage = 1;
totalPages = 1;
cache = {};
loadPage(currentTerm, currentPage, false, function(items, append) {
response(items);
});
}
},
minLength: 1,
focus: function() {
return false;
}
}).on('autocompleteopen', function() {
var menu = $('.ui-autocomplete');
menu.off('scroll.autocompletePager').on('scroll.autocompletePager', function() {
var scrollTimer = null;
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(function() {
if (menu.scrollTop() + menu.innerHeight() > menu[0].scrollHeight - 12) {
if (currentPage < totalPages) {
currentPage++;
loadPage(currentTerm, currentPage, true, function(items, append) {
var existing = menu.data('ui-autocomplete').menu.element.children('li').map(function() {
return $(this).data('item.autocomplete');
}).get();
var merged = existing.concat(items);
$('#search').autocomplete('option', 'source').call(null, { term: currentTerm }, function() {});
// 实际项目中应根据Autocomplete版本调整合并方式
});
}
}
}, 200);
});
});
需要注意的是,Autocomplete内部对菜单项的维护比较封闭,直接向已有菜单追加数据并不像普通列表那样简单。上面的示例只是展示缓存和请求锁的整合思路,实际合并时需要根据jQuery UI的具体版本调整菜单渲染逻辑。也可以考虑在loadPage的callback中重新构造完整的items数组,然后调用Autocomplete的search方法并配合缓存来避免重复请求。
四、调试验证与遗留问题
优化完成后,可以在IE9的开发者工具中打开网络面板,再次滚动下拉列表,观察请求的发起数量。正常情况下,每次滚动到底部只会产生一个请求,且请求的page参数会依次递增。如果仍然出现重复,建议在loadPage入口处添加console.log输出当前页码和isLoading状态,确认请求锁是否生效。
还有一些遗留问题需要留意。服务端必须正确返回totalPages字段,并且对page参数做合法性校验,防止超出范围。对于被abort掉的请求,jQuery的error回调仍会被触发,需要在error中判断请求是否被手动取消,避免弹出不必要的错误提示。可以通过检查xhr.statusText或textStatus是否为abort来区分。在IE9下滚动条位置的计算可能出现小数值,阈值设置得太小会导致提前触发加载,建议保留至少10像素的缓冲距离。
总体而言,IE9环境下的重复请求问题并非无解,只要从事件频率控制和请求时序控制两方面入手,再配合缓存策略,就能让Autocomplete的远程分页滚动加载恢复稳定。这套方案同样适用于其他需要滚动分页的老旧浏览器环境。
jQuery UI Autocomplete远程数据源分页重复请求修改时间:2026-09-25 18:22:17