當瀏覽器下載完頁面所需元素(html標記,css層疊樣式表,javascript,圖片)之后,會生成兩個東西:Dom樹和渲染樹。
Dom樹
Dom樹,主要是用來表示頁面的Dom結構。
渲染樹
渲染樹主要是用來表示頁面是如何進行渲染的。
Dom樹中,除了隱藏節點,其余的節點需要與渲染樹中的至少存在一個對應的節點。渲染樹中的每一個節點,被稱為幀或者是盒子。盒子具有內邊距,外邊距,邊框,位置等屬性。一旦渲染樹構建完成之后,瀏覽器就開始進行繪制頁面。
當Dom的變化影響到了元素的幾何屬性(寬和高等)——比如說修改了邊框的寬度,或者是修改了高度,又或者給文章增加了內容導致元素的高度增加等,會引起瀏覽器進行重新計算元素的幾何屬性,同樣,其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效。並重新構建渲染樹,這個過程稱為重排。完成重排之后,瀏覽器會重新繪制受影響的元素,這個過程被稱為重繪。
並不是所有的Dom變化會影響元素的幾何屬性,例如,改變背景色,不會影響元素的幾何屬性,因此,這個時候是不會發生重排,僅僅會發生重繪,因為,元素的不布局沒有發生變化。重排和重繪的代價都是昂貴的操作,他們會導致瀏覽器的UI線程卡頓,因此盡可能避免這類操作。
下面就是整個的基本流程圖:
什么時候回發生重排
正如前面所說的,當頁面的布局和幾何屬性發生改變的時候,就需要進行重排
。以下的情況也同樣會發生重排:
- 添加和或者刪除可見的DOM元素
- 元素的位置發生變化
- 元素的尺寸發生變化(包括:外邊距,內邊距,邊框厚度,寬度,高度等屬性發生改變)
- 內容發生變化(例如:內容增加引起高度變化或者是圖片被另外一個不同尺寸的圖片所替換)
- 頁面渲染器進行初始化的
- 瀏覽器窗口尺寸發生改變
根據改變的內容,渲染樹中相對應的部分也需要進行計算。有些改變會觸發整個頁面的重排:例如,當滾動條出現的時候。
由於每次的重排都會產生計算消耗,大多數瀏覽器通過隊列化修改並批量來優化 重排過程。但是,有些時候我們會強制進行刷新隊列,並要求計划任務立刻執行。這些方法包括以下 方法:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle()
以上的屬性和方法需要返回最新的布局信息,因此瀏覽器不得不執行渲染隊列中的“待處理變化”並觸發重排,以返回正確的值。因此,修改樣式的過程中,最好避免使用以上的屬性或者是方法。
如何優化重排效率
前面說到,重排和重繪的代價其實是非常昂貴的,因此,為了提高程序的響應速度,我們在平時的開發過程中應該盡量減少該操作的發生。為了減少重排或者是重繪的發生次數,我們可以有以下幾點的操作。
合並對Dom的多次修改
var el = document.getElementById('mydiv');
el.style.width = '300px';
el.style.height = '400px';
el.style.margin = '15px';
在以上打代碼中,我們能夠看到對元素的幾何屬性發生了三次的修改,因此,上面的代碼中會觸發三次的重排和重繪。因此,我們可以將對元素的三次修改合並成一次修改,這樣,就智慧觸發一次重排重繪。修改后的代碼如下:
var el = document.getElementById('mydiv');
el.style.cssText = 'width:30px;height:400px;margin:15px';
批量修改dom
當我們需要對Dom進行一系列操作時,可以通過以下步驟來減少重繪和重排:
-
是元素脫離文檔流
-
對其應用多重改變
-
把元素帶回文檔中
該過程會觸發兩次重排,第一步和第三步,如果忽略這兩個步驟,那么第二步的修改就會觸發多次的重排。這里要說的是,怎么才能使元素脫離文檔流?主要的又以下幾個方法:
-
隱藏元素,應用修改,重新顯示
-
使用文檔片段,在當前Dom之外構建一個子樹,再把拷貝會文檔
-
將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成后再替換原始元素
-
使用虛擬Dom
接下來,就演示一下有關如何脫離文檔流,來進行批量修改Dom的。我們先來構建一個場景:
<ul id="myList">
<li><a href="https://www.baidu.com">baidu</a></li>
<li><a href="https://www.qq.com">qq</a></li>
</ul>
上面是一個列表,假設我們要向上述列表中添加以下的數據:
var data = [
{
url: 'https://www.cnblogs.com/',
name: '博客園'
},
{
url: 'https://weibo.com/',
name: '新浪微博'
}
]
如果按照我們習慣性的思維,我們會這么去寫:
function appendDataToElement (appendToElement, data) {
var a;
var li;
for (var i = 0; i <data.length; i++) {
a = document.createElement('a');
li = document.createElement('li');
a.href = data[i].url;
a.appendChild(document.createTextNode(data[i].name));
li.appendChild(a);
appendToElement.appendChild(li);
}
}
var appendToElement = document.getElementById('myList');
appendDataToElement(appendToElement, data);
這樣寫雖說是能夠實現我們所想要的功能,但這樣做的話,會在每次appendChild之后,引起瀏覽器的重排和重繪,如果數據量特別大的時候,就會發生很多次的重排和重繪,因此我們需要對上面的方法進行修改:
var appendToElement = document.getElementById('myList');
appendToElement.style.display = 'none';
appendDataToElement(appendToElement, data);
appendToElement.style.display = 'block';
這樣話就只用渲染兩次,這就是上面所說的隱藏元素,應用修改,重新顯示;
接下來實現一下后面的兩種(除虛擬dom之外的方法)。
// 創建子樹的方法
var appendToElement = document.createDocumentFragment();
appendDataToElement(appendToElement, data);
document.getElementById('myList').appendChild(appendToElement);
// 將原始元素拷貝到一個脫離文檔的節點中
var old = document.getElementById('myList');
var clone = old.cloneNode();
appendDataToElement(appendToElement, data);
old.parentNode.replaceChild(clone, old);
針對於以上的方法,這邊推薦使用構建子樹的方法是創建子樹的方法,因為他們所產生的的Dom遍歷和重排次數最少,唯一潛在的問題就是文檔片段未被充分利用。
還有一種就是虛擬Dom,有關虛擬Dom這個,其實可以參考Vue和React等比較現代的前端開發的內容。