使用限制函数执行频率的函数代理
假设一个经典的CURD页面上,要做一个Ajax异步查询功能。
放一个查询按钮,点击查询,系统会到远程服务端请求数据,一秒之后返回查询结果。
很快,功能实现了!
但假如用户一秒内点击了三次查询,会发生什么?
为了解决这个问题,我们可能会在用户点击查询之后禁用查询按钮,或者在处理查询时上锁,返回结果后再把锁放开。
很好,做到这里,已足够日常使用。
这里只解决了一个问题:按钮的点击。而输入框的输入、选择框的变化、鼠标的移动、滚轮的滚动,这些事件触发频率高的问题怎么解决?
为了综合考虑,不重复自己,一个解决方案诞生了:使用限制函数执行频率的函数代理。
该函数API看起来跟setTimeout以及Promise很有点像,确实,但在形式上略有差异。
打算把它推荐给ES6,可惜前段时间给es-discuss讨论组发了好几次的String Padding API邮件都被防火墙拦回来了。
唉!只能在博客中说说。
函数
/**
* 为函数创建一个带帧速控制的代理函数
* 包含末尾的细节处理
* 注:此函数适用于调用间隔大于1ms的情形
* @static
* @method getFPSLimitedFunction
* @param {Function} accept - 指定的函数
* @param {Number} [fps=0] - 每秒最多执行的次数。(设interval为时间间隔毫秒数,该值等效于1000/interval)
* @param {Function} [reject=null] - 拒绝执行时的回调。
* @returns {Function} agent - 代理函数
* @throws {TypeError} called_non_callable - 若accept不是function时则抛出该错误
*/
function getFPSLimitedFunction(accept,reject,fps){
if(typeof accept!=="function"){
throw new TypeError(accept+" is not a function");
}
if(typeof reject!=="function"){
reject=null;
}
fps>>>=0;
var delay=Math.max(0,1000/fps),
locked=false,
timer=0,
rejectedQueue=[],
lastAcceptedTime=0,
lastRejectedTime=0;
var lock=function(){
locked=true;
};
var unlock=function(){
locked=false;
clearTimeout(timer);
timer=setTimeout(checkRejectedCalls,delay);
};
var checkRejectedCalls=function(){
if(lastAcceptedTime<lastRejectedTime){
var l=rejectedQueue.length,
i,
call;
if(l>0){
if(typeof reject==="function"){
for(i=0;i<l-1;i++){
call=rejectedQueue[i];
try{
reject.apply(call[0],call[1]);
}catch(e){
setTimeout(function(){throw e;},0);
}
}
}
call=rejectedQueue[l-1];
try{
accept.apply(call[0],call[1]);
}catch(e){
setTimeout(function(){throw e;},0);
}
}
}
rejectedQueue.length=0;
};
var handleRejectedCalls=function(){
if(typeof reject==="function"){
var l=rejectedQueue.length,
i,
call;
for(i=0;i<l;i++){
call=rejectedQueue[i];
try{
reject.apply(call[0],call[1]);
}catch(e){
setTimeout(function(){throw e;},0);
}
}
}
rejectedQueue.length=0;
};
var agent=function(){
if(locked){
lastRejectedTime=Date.now();
rejectedQueue.push([this,arguments]);
return;
}
clearTimeout(timer);
handleRejectedCalls();
lock();
lastAcceptedTime=Date.now();
accept.apply(this,arguments);
setTimeout(unlock,delay);
};
agent.toString=function(){return accept.toString();};
if(accept.toSource){
agent.toSource=function(){return accept.toSource();};
}
return agent;
}
用例
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// demo1 限制事件监听函数调用频率
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//为document添加鼠标移动事件监听,监听每秒最多执行25次
var document_mousemoveHandler=getFPSLimitedFunction(function(event){
//TODO 处理鼠标移动
},null,25);
document.addEventListener("mousemove",document_mousemoveHandler);
//为查询按钮添加点击事件监听,监听每秒最多执行1次
var button_clickHandler=getFPSLimitedFunction(function(event){
//TODO 查询数据
},null,1);
document.querySelector("#searchButton").addEventListener("click",button_clickHandler);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// demo2: 为document添加鼠标移动事件监听,监听每秒最多执行60次
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//demo2
function doSomethingWithIntervalAndTimes(something,interval,times){
var count=0;
var timer=setInterval(function(){
count++;
if(count>=times){
clearTimeout(timer);
}
something(count,times);
},interval);
return timer;
}
//1秒最多接受一次
var showMessage=getFPSLimitedFunction(
function accept(s){//接受业务
console.log("accepted: "+s);
},
function reject(s){//处理被拒绝
console.warn("rejected: "+s);
},
1
);
//每秒隔100ms一次(10fps),理论上每秒1次被接受,9次被拒绝
doSomethingWithIntervalAndTimes(function(currentCount,repeatCount){
showMessage(currentCount);
},100,100);