使用限制函數執行頻率的函數代理

使用限制函數執行頻率的函數代理

假設一個經典的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);


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章