JavaScript最佳實踐:效能
注意作用域
避免全域性查詢
一個例子:
function updateUI(){ var imgs = document.getElementByTagName("img"); for(var i=0, len=imgs.length; i<len; i++){ imgs[i].title = document.title + " image " + i; } var msg = document.getElementById("msg"); msg.innnerHTML = "Update complete."; }
該函式可能看上去完全正常,但是它包含了三個對於全域性document物件的引用。如果在頁面上有多個圖片,那麼for迴圈中的document引用就會被執行多次甚至上百次,每次都會要進行作用域鏈查詢。通過建立一個指向document物件的區域性變數,就可以通過限制一次全域性查詢來改進這個函式的效能:
function updateUI(){ var doc = document; var imgs = doc.getElementByTagName("img"); for(var i=0, len=imgs.length; i<len; i++){ imgs[i].title = doc.title + " image " + i; } var msg = doc.getElementById("msg"); msg.innnerHTML = "Update complete."; }
這裡,首先將document物件存在本地的doc變數中;然後在餘下的程式碼中替換原來的document。與原來的版本相比,現在的函式只有一次全域性查詢,肯定更快。
選擇正確方法
1.避免不必要的屬性查詢
獲取常量值是非常高效的過程
var value = 5; var sum = 10 + value; alert(sum);
該程式碼進行了四次常量值查詢:數字5,變數value,數字10和變數sum。
在JavaScript中訪問陣列元素和簡單的變數查詢效率一樣。所以以下程式碼和前面的例子效率一樣:
var value = [5,10]; var sum = value[0] + value[1]; alert(sum);
物件上的任何屬性查詢都比訪問變數或者陣列花費更長時間,因為必須在原型鏈中對擁有該名稱的屬性進行一次搜素。屬性查詢越多,執行時間就越長。
var values = {first: 5, second: 10}; var sum = values.first + values.second; alert(sum);
這段程式碼使用兩次屬性查詢來計算sum的值。進行一兩次屬性查詢並不會導致顯著的效能問題,但是進行成百上千次則肯定會減慢執行速度。
注意獲取單個值的多重屬性查詢。例如:
var query = window.location.href.substring(window.location.href.indexOf("?"));
在這段程式碼中,有6次屬性查詢:window.location.href.substring()有3次,window.location.href.indexOf()又有3次。只要數一數程式碼中的點的數量,就可以確定查詢的次數了。這段程式碼由於兩次用到了window.location.href,同樣的查詢進行了兩次,因此效率特別不好。
一旦多次用到物件屬性,應該將其儲存在區域性變數中。之前的程式碼可以如下重寫:
var url = window.locaiton.href; var query = url.substring(url.indexOf("?"));
這個版本的程式碼只有4次屬性查詢,相對於原始版本節省了33%。
一般來講,只要能減少演算法的複雜度,就要儘可能減少。儘可能多地使用區域性變數將屬性查詢替換為值查詢,進一步獎,如果即可以用數字化的陣列位置進行訪問,也可以使用命名屬性(諸如NodeList物件),那麼使用數字位置。
2.優化迴圈
一個迴圈的基本優化步驟如下所示。
(1)減值迭代——大多數迴圈使用一個從0開始、增加到某個特定值的迭代器。在很多情況下,從最大值開始,在迴圈中不斷減值的迭代器更加高效。
(2)簡化終止條件——由於每次迴圈過程都會計算終止條件,所以必須保證它儘可能快。也就是說避免屬性查詢或其他操作。
(3)簡化迴圈體——迴圈是執行最多的,所以要確保其最大限度地優化,確保其他某些可以被很容易移除迴圈的密集計算。
(4使用後測試迴圈——最常用for迴圈和while迴圈都是前測試迴圈。而如do-while這種後測試迴圈,可以避免最初終止條件的計算,因此執行更快。
以下是一個基本的for迴圈:
for(var i=0; i < value.length; i++){ process(values[i]); }
這段程式碼中變數i從0遞增到values陣列中的元素總數。迴圈可以改為i減值,如下所示:
for(var i=value.length -1; i >= 0; i--){ process(values[i]); }
終止條件從value.length簡化成了0。
迴圈還能改成後測試迴圈,如下:
var i=values.length -1; if (i> -1){ do{ process(values[i]) }while(--i>=0) //此處有個勘誤,書上終止條件為(--i>0),經測試,(--i>=0)才是正確的 }
此處最主要的優化是將終止條件和自減操作符組合成了單個語句,迴圈部分已經優化完全了。
記住使用“後測試”迴圈時必須確保要處理的值至少有一個,空陣列會導致多餘的一次迴圈而“前測試”迴圈則可以避免。
3.展開迴圈
當迴圈的次數是確定的,消除迴圈並使用多次函式呼叫往往更快。假設values陣列裡面只有3個元素,直接對每個元素呼叫process()。這樣展開迴圈可以消除建立迴圈和處理終止條件的額外開銷,使程式碼執行更快。
//消除迴圈 process(values[0]); process(values[1]); process(values[2]);
如果迴圈中的迭代次數不能事先確定,那可以考慮使用一種叫做Duff裝置的技術。Duff裝置的基本概念是通過計算迭代的次數是否為8的倍數將一個迴圈展開為一系列語句。
Andrew B.King提出了一個更快的Duff裝置技術,將do-while迴圈分成2個單獨的迴圈。以下是例子:
var iterations = Math.floor(values.length / 8); var leftover = values.length % 8; var i = 0; if(leftover>0){ do{ process(values[i++]); }while(--leftover > 0); } do{ process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); }while(--iterations > 0);
在這個實現中,剩餘的計算部分不會在實際迴圈中處理,而是在一個初始化迴圈中進行除以8的操作。當處理掉了額外的元素,繼續執行每次呼叫8次process()的主迴圈。
針對大資料集使用展開迴圈可以節省很多時間,但對於小資料集,額外的開銷則可能得不償失。它是要花更多的程式碼來完成同樣的任務,如果處理的不是大資料集,一般來說不值得。
4.避免雙重解釋
當JavaScript程式碼想解析KavaScript的時候就會存在雙重解釋懲罰。當使用eval()函式或者是Function建構函式以及使用setTimeout()傳一個字串引數時都會發生這種情況。
//某些程式碼求值——避免!! eval("alert('Hello world!')"); //建立新函式——避免!! var sayHi = new Function("alert('Hello world!')"); //設定超時——避免!! setTimeout("alert('Hello world!')", 500);
在以上這些例子中,都要解析包含了JavaScript程式碼的字串。這個操作是不能在初始的解析過程中完成的,因為程式碼是包含在字串中的,也就是說在JavaScript程式碼執行的同時必須新啟動一個解析器來解析新的程式碼。例項化一個新的解析器有不容忽視的開銷,所以這種程式碼要比直接解析慢得多。
//已修正 alert('Hello world!'); //建立新函式——已修正 var sayHi = function(){ alert('Hello world!'); }; //設定一個超時——已修正 setTimeout(function(){ alert('Hello world!'); }, 500);
如果要提高程式碼效能,儘可能避免出現需要按照JavaScript解析的字串。
5.效能的其他注意事項
(1)原生方法較快
(2)Switch語句較快
(3)位運算子較快
最小化語句數
1.多個變數宣告
//4個語句——很浪費 var count = 5; var color = "blue"; var values = [1,2,3]; var now = new Date(); //一個語句 var count = 5, color = "blue", values = [1,2,3], now = new Date();
2.插入迭代值
當使用迭代值的時候,儘可能合併語句。
var name = values[i]; i++;
前面這2句語句各只有一個目的:第一個從values陣列中獲取值,然後儲存在name中;第二個給變數i增加1.這兩句可以通過迭代值插入第一個語句組合成一個語句。
var name = values[i++];
3.使用陣列和物件字面量
//用4個語句建立和初始化陣列——浪費 var values = new Array(); values[0] = 123; values[1] = 456; values[2] = 789; //用4個語句建立和初始化物件——浪費 var person = new Object(); person.name = "Nicholas"; person.age = 29; person.sayName = function(){ alert(this.name); };
這段程式碼中,只建立和初始化了一個陣列和一個物件。各用了4個語句:一個呼叫建構函式,其他3個分配資料。其實可以很容易地轉換成使用字面量的形式。
//只有一條語句建立和初始化陣列 var values = [13,456,789]; //只有一條語句建立和初始化物件 var person = { name : "Nicholas", age : 29, sayName : function(){ alert(this.name); } };
重寫後的程式碼只包含兩條語句,減少了75%的語句量,在包含成千上萬行JavaScript的程式碼庫中,這些優化的價值更大。
只要有可能,儘量使用陣列和物件的字面量表達方式來消除不必要的語句。
優化DOM互動
1.最小化現場更新
一旦你需要訪問的DOM部分是已經顯示的頁面的一部分,那麼你就是在進行一個現場更新。現場更新進行得越多,程式碼完成執行所花的事件就越長。
var list = document.getElementById('myList'), item, i; for (var i = 0; i < 10; i++) { item = document.createElement("li"); list.appendChild(item); item.appendChild(document.createTextNode("Item" + i)); }
這段程式碼為列表新增了10個專案。新增每個專案時,都有2個現場更新:一個新增li元素,另一個給它新增文字節點。這樣新增10個專案,這個操作總共要完成20個現場更新。
var list = document.getElementById('myList'), fragment = document.createDocumentFragment(), item, i; for (var i = 0; i < 10; i++) { item = document.createElement("li"); fragment.appendChild(item); item.appendChild(document.createTextNode("Item" + i)); } list.appendChild(fragment);
在這個例子中只有一次現場更新,它發生在所有專案都建立好之後。文件片段用作一個臨時的佔位符,放置新建立的專案。當給appendChild()傳入文件片段時,只有片段中的子節點被新增到目標,片段本身不會被新增的。
一旦需要更新DOM,請考慮使用文件片段來構建DOM結構,然後再將其新增到現存的文件中。
2.使用innerHTML
有兩種在頁面上建立DOM節點的方法:使用諸如createElement()和appendChild()之類的DOM方法,以及使用innerHTML。對於小的DOM更改而言,兩種方法效率都差不多。然而,對於大的DOM更改,使用innerHTML要比使用標準DOM方法建立同樣的DOM結構快得多。
當把innerHTML設定為某個值時,後臺會建立一個HTML解析器,然後使用內部的DOM呼叫來建立DOM結構,而非基於JavaScript的DOM呼叫。由於內部方法是編譯好的而非解釋執行的,所以執行快得多。
var list = document.getElementById("myList"); html = ""; i; for (i=0; i < 10; i++){ html += "<li>Item " + i +"</li>"; } list.innerHTML = html;
使用innerHTML的關鍵在於(和其他的DOM操作一樣)最小化呼叫它的次數。
var list = document.getElementById("myList"); i; for (i=0; i < 10; i++){ list.innerHTML += "<li>Item " + i +"</li>"; //避免!!! }
這段程式碼的問題在於每次迴圈都要呼叫innerHTML,這是極其低效的。呼叫innerHTML實際上就是一次現場更新。構建好一個字串然後一次性呼叫innerHTML要比呼叫innerHTML多次快得多。
3.使用事件代理(根據第13章的概念,我認為此處應為“事件委託”更為妥當)
4.注意HTMLCollection
任何時候要訪問HTMLCollection,不管它是一個屬性還是一個方法,都是在文件上進行一個查詢,這個查詢開銷很昂貴。
var images = document.getElementsByTagName("img"), image, i,len; for (i=0, len=images.length; i < len; i++){ image = images[i]; //處理 }
將length和當前引用的images[i]存入變數,這樣就可以最小化對他們的訪問。發生以下情況時會返回HTMLCollection物件:
- 進行了對getElementsByTagName()的呼叫;
- 獲取了元素的childNodes屬性;
- 獲取了元素的attributes屬性;
- 訪問了特殊的集合,如document.forms、document.images等。
相關文章
- JavaScript 最佳實踐JavaScript
- Golang效能最佳化實踐Golang
- 10個Spring Boot效能最佳實踐Spring Boot
- 2022 前端效能優化最佳實踐前端優化
- HBase最佳實踐-讀效能優化策略優化
- HarmonyOS:應用效能最佳化實踐
- MySQL8.0效能最佳化(實踐)MySql
- TiDB 效能分析&效能調優&最佳化實踐大全TiDB
- JavaScript效能最佳化JavaScript
- Taro:高效能小程式的最佳實踐
- Taro | 高效能小程式的最佳實踐
- ASP.NET Core 效能優化最佳實踐ASP.NET優化
- 前端效能最佳化實踐方向與方法前端
- Hadoop YARN:排程效能最佳化實踐HadoopYarn
- 14個Flink SQL效能最佳化實踐分享SQL
- 17 個提高效能的 Flutter 最佳實踐Flutter
- 阿里雲 VPC 內網效能測試最佳實踐阿里內網
- 45個有用的JavaScript技巧,竅門和最佳實踐JavaScript
- 編寫高效能 Java 程式碼的最佳實踐Java
- 效能診斷利器JProfiler快速入門和最佳實踐
- Redis大叢集擴容效能最佳化實踐Redis
- 達達快送小程式效能最佳化實踐
- 基於Ascend C的FlashAttention運算元效能最佳化最佳實踐
- 別再被坑了! JavaScript型別檢測的最佳實踐JavaScript型別
- AutoMapper 最佳實踐APP
- 《.NET最佳實踐》
- Django 最佳實踐Django
- metaq最佳實踐
- springDataJpa 最佳實踐Spring
- KeyPath 最佳實踐
- Pika最佳實踐
- SnapKit 最佳實踐APK
- JDBC 最佳實踐JDBC
- Kafka最佳實踐Kafka
- Iptables 最佳實踐 !
- Serilog 最佳實踐
- Flutter 最佳實踐Flutter
- Java最佳實踐Java
- MongoDB 最佳實踐MongoDB