JavaScript之優化DOM

来源:https://www.cnblogs.com/wind-lanyan/archive/2018/03/17/8588544.html
-Advertisement-
Play Games

優化DOM得從重繪和重排講起,long long ago... 1、重繪和重排 1.1 重繪和重排是什麼 重繪是指一些樣式的修改,元素的位置和大小都沒有改變; 重排是指元素的位置或尺寸發生了變化,瀏覽器需要重新計算渲染樹,而新的渲染樹建立後,瀏覽器會重新繪製受影響的元素。 1.2 瀏覽器渲染頁面 去 ...


優化DOM得從重繪和重排講起,long long ago...

 

1、重繪和重排

1.1 重繪和重排是什麼

重繪是指一些樣式的修改,元素的位置和大小都沒有改變;

重排是指元素的位置或尺寸發生了變化,瀏覽器需要重新計算渲染樹,而新的渲染樹建立後,瀏覽器會重新繪製受影響的元素。

1.2 瀏覽器渲染頁面

去參加面試總會被問到一個問題,那就是“向瀏覽器輸入一行url會發生什麼?”,這個問題的答案除了要回答網路方面的知識還牽扯到瀏覽器渲染頁面問題。當我們的瀏覽器接收到從伺服器響應的頁面之後便開始逐行渲染,遇到css的時候會非同步的去計算屬性值,再繼續向下解析dom解析完畢之後形成一顆DOM樹,將非同步計算好的樣式(樣式盒子)與DOM樹相結合便成為了一個Render樹,再由瀏覽器繪製在頁面上。DOM樹與Render樹的區別在於:樣式為display:none;的節點會在DOM樹中而不在渲染樹中。瀏覽器繪製了之後便開始解析js文件,根據js來確定是否重繪和重排。

1.3 引起重繪和重排的原因

產生重繪的因素:

  • 改變visibility、outline、背景色等樣式屬性,並沒有改變元素大小、位置等。瀏覽器會根據元素的新屬性重新繪製。

產生重排的因素:

 

  • 內容改變
    • 文本改變或圖片尺寸改變
  • DOM元素的幾何屬性的變化
    • 例如改變DOM元素的寬高值時,原渲染樹中的相關節點會失效,瀏覽器會根據變化後的DOM重新排建渲染樹中的相關節點。如果父節點的幾何屬性變化時,還會使其子節點及後續兄弟節點重新計算位置等,造成一系列的重排。
  • DOM樹的結構變化
    • 添加DOM節點、修改DOM節點位置及刪除某個節點都是對DOM樹的更改,會造成頁面的重排。瀏覽器佈局是從上到下的過程,修改當前元素不會對其前邊已經遍歷過的元素造成影響,但是如果在所有的節點前添加一個新的元素,則後續的所有元素都要進行重排。
  • 獲取某些屬性
    • 除了渲染樹的直接變化,當獲取一些屬性值時,瀏覽器為取得正確的值也會發生重排,這些屬性包括:offsetTopoffsetLeft、 offsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeight、 clientTopclientLeftclientWidthclientHeightgetComputedStyle()
  • 瀏覽器視窗尺寸改變
    • 視窗尺寸的改變會影響整個網頁內元素的尺寸的改變,即DOM元素的集合屬性變化,因此會造成重排。
  • 滾動條的出現(會觸發整個頁面的重排)

總之你要知道,js是單線程的,重繪和重排會阻塞用戶的操作以及影響網頁的性能,當一個頁面發生了多次重繪和重排比如寫一個定時器每500ms改變頁面元素的寬高,那麼這個頁面可能會變得越來越卡頓,我們要儘可能的減少重繪和重排。那麼我們對於DOM的優化也是基於這個開始。

 

2、優化  

2.1 減少訪問

減少訪問次數自然是想到緩存元素,但是要註意

var ele = document.getElementById('ele');

這樣並不是對ele進行緩存,每一次調用ele還是相當於訪問了一次id為ele的節點。

2.1.1 緩存NodeList

var foods = document.getElementsByClassName('food');

我們可以用foods[i]來訪問第i個class為food的元素,不過這裡的foods並不是一個數組,而是一個NodeList。NodeList是一個類數組,保存了一些有序的節點並可以通過位置來訪問這些節點。NodeList對象是動態的,每一次訪問都會運行一次基於文檔的查詢。所以我們要儘量減少訪問NodeList的次數,可以考慮將NodeList的值緩存起來。

// 優化前
var lis = document.getElementsByTagName('li');

for(var i = 0; i < lis.length; i++) {
     // do something...  
}

// 優化後,將length的值緩存起來就不會每次都去查詢length的值
var lis = document.getElementsByTagName('li');

for(var i = 0, len = lis.length; i < len; i++) {
     // do something...  
}

而且由於NodeList是動態變化的,所以如果不緩存可能會引起死迴圈,比如一邊添加元素,一邊獲取NodeList的length。

2.1.2 改變選擇器

獲取元素最常見的有兩種方法,getElementsByXXX()和queryselectorAll(),這兩種選擇器區別是很大的,前者是獲取動態集合,後者是獲取靜態集合,舉個例子。

// 假設一開始有2個li
var lis = document.getElementsByTagName('li');  // 動態集合
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 輸出結果:2, 3, 4


var lis = document.querySelector('li');  // 靜態集合 
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 輸出結果:2, 2, 2

對靜態集合的操作不會引起對文檔的重新查詢,相比於動態集合更加優化。

2.1.3 避免不必要的迴圈

// 優化前
for(var i = 0; i < 10; i++) {
document.getElementById('ele').innerHTML += 'a';
} 

// 優化後
var str = '';
for(var i = 0; i < 10; i++) {
str += 'a';
}
document.getElementById('ele').innerHTML = str;

優化前的代碼訪問了10次ele元素,而優化後的代碼只訪問了一次,大大的提高了效率。

2.1.4 事件委托

js中的事件函數都是對象,如果事件函數過多會占用大量記憶體,而且綁定事件的DOM元素越多會增加訪問dom的次數,對頁面的交互就緒時間也會有延遲。所以誕生了事件委托,事件委托是利用了事件冒泡,只指定一個事件處理程式就可以管理某一類型的所有事件。

// 事件委托前
var lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++) {
   lis[i].onclick = function() {
      console.log(this.innerHTML);
   };  
}    

// 事件委托後
var ul = document.getElementsByTagName('ul')[0];
ul.onclick = function(event) {
   console.log(event.target.innerHTML);
};

事件委托前我們訪問了lis.length次li,而採用事件委托之後我們只訪問了一次ul。

2.2 減少重繪重排

2.2.1 改變一個dom節點的多個樣式

我們想改變一個div元素的寬度和高度,通常做法可以是這樣

var div = document.getElementById('div1');
div.style.width = '220px';
div.style.height = '300px';

以上操作改變了元素的兩個屬性,訪問了三次dom,觸發兩次重排與兩次重繪。我們說過優化是減少訪問次數以及減少重繪重排次數,從這個出發點可不可以只訪問一次元素以及重排次數降低到1呢?顯然是可以的,我們可以在css里寫一個class

/* css
.change {
    width: 220px;
    height: 300px;
}
*/
document.getElementById('div').className = 'change';

這樣就達到了一次操作多個樣式

2.2.2  批量修改dom節點樣式

上面代碼的情況是針對於一個dom節點的,如果我們要改變一個dom集合的樣式呢?

第一時間想到的方法是遍歷集合,給每個節點加一個className。再想想這樣豈不是訪問了多次dom節點?想想文章開頭說的dom樹和渲染樹的區別,如果一個節點的display屬性為none那麼這個節點不會存在於render樹中,意味著對這個節點的操作也不會影響render樹進而不會引起重繪和重排,基於這個思路我們可以實現優化:

  • 將待修改的集合的父元素display: none;
  • 之後遍歷修改集合節點
  • 將集合父元素display: block;
// 假設增加的class為.change
var lis = document.getElementsByTagName('li');  
var ul = document.getElementsByTagName('ul')[0];

ul.style.display = 'none';

for(var i = 0; i < lis.length; i++) {
    lis[i].className = 'change';  
}

ul.style.display = 'block';

 

3、總結

  • 減少訪問dom的次數
    • 緩存節點屬性值
    • 選擇器的使用
    • 避免不必要的迴圈
    • 事件委托
  • 減少重繪與重排
    • 使用className改變多個樣式
    • 使父元素脫離文檔流再恢復

如果以後看到其他優化方案我會更新,歡迎大家與我交流。

 

參考文檔:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • DrawerLayout是Support Library包中實現了側滑菜單效果的控制項,可以說DrawerLayout是因為第三方控制項如SlidingMenu等出現之後,google借鑒而出現的產物。DrawerLayout分為側邊菜單和主內容區兩部分,側邊菜單可以根據手勢展開與隱藏(drawerLa ...
  • Array 類型: 檢測數組: console.log(myarr instanceof Array) //true toString()方法會返回由數組中每個值的字元串形式拼接而成的一個以逗號分隔的字元串 valueOf()返回的還是數組 數組的棧方法: push方法從數組末尾添加一項,並返回修改 ...
  • 我們創建的每個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個原型對象,而這個原型對象中擁有的屬性和方法可以被所以實例共用。 一、理解原型對象 無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個 prototype屬性,這個屬性指向函數的原型對象。 ...
  • >官網:[https://qiu8310.github.io/minapp/](https://qiu8310.github.io/minapp/) >作者:[Mora](https://github.com/qiu8310) 在原生小程式開發中,數據流是單向的,無法雙向綁定,但是要實現雙向綁定... ...
  • map標簽的用途:是與img標簽綁定使用的,常被用來賦予給客戶端圖像某處區域特殊的含義,點擊該區域可跳轉到新的文檔。 因為map標簽是與img標簽綁定使用的,所以我們需要給map標簽添加ID和name屬性,讓img標簽中的usemap屬性引用map標簽中的id或者name屬性(由於瀏覽器的不同,us ...
  • Vue對象,按照現在的學習進度,可以分為: 其中el代表作用的HTML元素; data代表el中的所有數據; methods代表el中所有元素上的事件; computed代表計算屬性,用於計算data內變數產生新的需要的數據,例如平均數等,有別於methods,computed的數值只有data改變 ...
  • charAt() 獲取字元串中特定索引處的字元; toupperCase() 將字元串的所有字元轉換成大寫字母; indexOf() 返回字元串中特定字元串第一次出現的位置 substring() 返回字元串的某個子串 slice() 返回字元串中的某個子串,支持負數參數(字元串中倒數第一個字元定為 ...
  • JQuery的查詢手冊 : 一, JQuery的用法 1. 首先要下載Jquery的js文件,併在需要使用JQuery的html文件的<head>標簽載入該js文件 : <script type="text/javascript" src="js/jquery.js"></script> 併在下一行 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...