[1]常見場景 [2]函數防抖 [3]函數節流 [4]數組分塊 [5] ...
前面的話
javascript中的函數大多數情況下都是由用戶主動調用觸發的,除非是函數本身的實現不合理,否則一般不會遇到跟性能相關的問題。但在一些少數情況下,函數的觸發不是由用戶直接控制的。在這些場景下,函數有可能被非常頻繁地調用,而造成大的性能問題。解決性能問題的處理辦法就是函數節流和函數防抖。本文將詳細介紹函數節流和函數防抖
常見場景
下麵是函數被頻繁調用的常見的幾個場景
1、mousemove事件。如果要實現一個拖拽功能,需要一路監聽 mousemove 事件,在回調中獲取元素當前位置,然後重置 dom 的位置來進行樣式改變。如果不加以控制,每移動一定像素而觸發的回調數量非常驚人,回調中又伴隨著 DOM 操作,繼而引發瀏覽器的重排與重繪,性能差的瀏覽器可能就會直接假死。
2、window.onresize事件。為window對象綁定了resize事件,當瀏覽器視窗大小被拖動而改變的時候,這個事件觸發的頻率非常之高。如果在window.onresize事件函數里有一些跟DOM節點相關的操作,而跟DOM節點相關的操作往往是非常消耗性能的,這時候瀏覽器可能就會吃不消而造成卡頓現象
3、射擊游戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
4、搜索聯想(keyup事件)
5、監聽滾動事件判斷是否到頁面底部自動載入更多(scroll事件)
對於這些情況的解決方案就是函數節流(throttle)或函數去抖(debounce),核心其實就是限制某一個方法的頻繁觸發
函數防抖
函數防抖的原理是將即將被執行的函數用setTimeout延遲一段時間執行。對於正在執行的函數和新觸發的函數衝突問題有兩種處理,也分別對應了定時器管理的兩種機制
第一種是只要當前函數沒有執行完成,任何新觸發的函數都會被忽略,簡易代碼如下
function debounce(method, context) { //忽略新函數 if(method.tId){ return false; } method.tId = setTimeout(function() { method.call(context); }, 1000); }
第二種是只要有新觸發的函數,就立即停止執行當前函數,轉而執行新函數,簡易代碼如下
function debounce(method, context) { //停止當前函數 clearTimeout(method.tId); method.tId = setTimeout(function() { method.call(context); }, 1000); }
當然,不論是哪種處理,函數去抖的目的是讓要執行的函數停止一段時間之後才執行
下麵是一個比較完整的防抖函數(debounce),該函數接受2個參數,第一個參數為需要被延遲執行的函數,第二個參數為延遲執行的時間
var debounce = function ( fn, interval ) { var _self = fn, // 保存需要被延遲執行的函數引用 timer, // 定時器 firstTime = true; // 是否是第一次調用 return function () { var args = arguments, _me = this; if ( firstTime ) { // 如果是第一次調用,不需延遲執行 _self.apply( me, args); return firstTime = false; } if ( timer ) { // 如果定時器還在,說明前一次延遲執行還沒有完成 return false; } timer = setTimeout(function () { // 延遲一段時間執行 clearTimeout(timer); timer = null; _self.apply(_me, args); }, interval || 500 ); }; }; window.onresize = debounce(function(){ console.log( 1 ); }, 500 );
函數節流
函數節流使得連續的函數執行,變為固定時間段間斷地執行。關於節流的實現,有兩種主流的實現方式,一種是使用時間戳,一種是設置定時器
【使用時間戳】
觸發事件時,取出當前的時間戳,然後減去之前的時間戳(最一開始值設為 0 ),如果大於設置的時間周期,就執行函數,然後更新時間戳為當前的時間戳,如果小於,就不執行
function throttle(func, wait) { var context, args; var previous = 0; return function() { var now = +new Date(); context = this; args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } }
【使用定時器】
觸發事件時,設置一個定時器,再觸發事件的時候,如果定時器存在,就不執行,直到定時器執行,然後執行函數,清空定時器,這樣就可以設置下個定時器
function throttle(func, wait) { var timeout,args,context; var previous = 0; return function() { context = this; args = arguments; if (!timeout) { timeout = setTimeout(function(){ timeout = null; func.apply(context, args) }, wait) } } }
數組分塊
在前面關於函數節流和函數防抖的討論中,提供了限制函數被頻繁調用的解決方案。下麵將遇到另外一個問題,某些函數確實是用戶主動調用的,但因為一些客觀的原因,這些函數會嚴重地影響頁面性能
一個例子是創建WebQQ的QQ好友列表。列表中通常會有成百上千個好友,如果一個好友用一個節點來表示,在頁面中渲染這個列表的時候,可能要一次性往頁面中創建成百上千個節點
在短時間內往頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消,看到的結果往往就是瀏覽器的卡頓甚至假死。代碼如下:
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假設 ary 裝載了 1000 個好友的數據 }; var renderFriendList = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); } }; renderFriendList( ary );
這個問題的解決方案之一是數組分塊技術,下麵的timeChunk函數讓創建節點的工作分批進行,比如把1秒鐘創建1000個節點,改為每隔200毫秒創建8個節點
數組分塊是一種使用定時器分割迴圈的技術,為要處理的項目創建一個隊列,然後使用定時器取出下一個要處理的項目進行處理,接著再設置另一個定時器
在數組分塊模式中,array變數本質上就是一個“待辦事宜”列表,它包含了要處理的項目。使用shift()方法可以獲取隊列中下一個要處理的項目,然後將其傳遞給某個函數。如果在隊列中還有其他項目,則設置另一個定時器,並通過arguments.callee調用同一個匿名函數
數組分塊的重要性在於它可以將多個項目的處理在執行隊列上分開,在每個項目處理之後,給予其他的瀏覽器處理機會運行,這樣就可能避免長時間運行腳本的錯誤。一旦某個函數需要花50ms以上的時間完成,那麼最好看看能否將任務分割為一系列可以使用定時器的小任務
下麵是數組分塊模式的簡易代碼
function chunk(array,process,context){ setTimeout(function(){ //取出下一個條目並處理 var item = array.shift(); process.call(context,item); //若還有條目,再設置另一個定時器 if(array.length > 0){ setTimeout(arguments.callee,100); } },100); }
var data = [1,2,3,4,5,6,7,8,9,0]; function printValue(item){ var div = document.getElementById('myDiv'); div.innerHTML += item + '<br>'; } chunk(data.concat(),printValue);
下麵是數組分塊的詳細代碼,timeChunk函數接受3個參數,第1個參數是創建節點時需要用到的數據,第2個參數是封裝了創建節點邏輯的函數,第3個參數表示每一批創建的節點數量
var timeChunk = function( ary, fn, count ){ var obj,t; var len = ary.length; var start = function(){ for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){ var obj = ary.shift(); fn( obj ); } }; return function(){ t = setInterval(function(){ if ( ary.length === 0 ){ // 如果全部節點都已經被創建好 return clearInterval( t ); } start(); }, 200 ); // 分批執行的時間間隔,也可以用參數的形式傳入 }; };
最後進行一些小測試,假設有1000個好友的數據,利用timeChunk函數,每一批只往頁面中創建8個節點
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); }; var renderFriendList = timeChunk( ary, function( n ){ var div = document.createElement( 'div' ); div.innerHTML = n; document.body.appendChild( div ); }, 8 ); renderFriendList();