寫在前面:
大家都知道DOM的操作很昂貴。
然后貴在什么地方呢?
一、訪問DOM元素
二、修改DOM引起的重繪重排
一、訪問DOM
像書上的比喻:把DOM和JavaScript(這里指ECMScript)各自想象為一個島嶼,它們之間用收費橋梁連接,ECMAScript每次訪問DOM,都要途徑這座橋,並交納“過橋費”,訪問DOM的次數越多,費用也就越高。因此,推薦的做法是盡量減少過橋的次數,努力待在ECMAScript島上。我們不可能不用DOM的接口,那么,怎樣才能提高程序的效率?
- 既然無法避免,那就減少訪問。(width、offsetTop、left。。。能少就少,可以緩存起來的,就緩存)
// code1錯誤 console.time(1); for(var i = 0; i < times; i++) { document.getElementById('div1').innerHTML += 'a'; } console.timeEnd(1); // code2正確 console.time(2); var str = ''; for(var i = 0; i < times; i++) { str += 'a'; } document.getElementById('div2').innerHTML = str; console.timeEnd(2);
//////////////////////// - html集合&遍歷DOM
html集合類似數組,但是跟數組還是不一樣的。如: document.getElementsByTagName('a') 返回的html集合。這個集合是實時更新的,即后面代碼修改了DOM,會反映在這個html集合里面。可嘗試代碼。
<body> <ul id='fruit'> <li> apple </li> <li> orange </li> <li> banana </li> </ul> </body> <script type="text/javascript"> var lis = document.getElementsByTagName('li'); var peach = document.createElement('li'); peach.innerHTML = 'peach'; document.getElementById('fruit').appendChild(peach); console.log(lis.length); // 4 </script>
正因為這個原因:html集合,讀取 length 屬性比數組消耗大多了。
要解決這個問題並不難,在遍歷DOM集合的時候,緩存length就好了。不要每次使用就獲取,主要體現在for循環中(你應該知道,for循環中,每一次都會執行判讀語句,讀取length)
console.time(0); var lis0 = document.getElementsByTagName('li'); var str0 = ''; for(var i = 0; i < lis0.length; i++) { str0 += lis0[i].innerHTML; } console.timeEnd(0); console.time(1); var lis1 = document.getElementsByTagName('li'); var str1 = ''; for(var i = 0, len = lis1.length; i < len; i++) { str1 += lis1[i].innerHTML; } console.timeEnd(1);
二、重繪重排
1.什么是重繪重排?
瀏覽器下載完頁面中的所有組件——HTML標記、JavaScript、CSS、圖片之后會解析生成兩個內部數據結構——DOM樹
和渲染樹
。
在文檔初次加載時,瀏覽器引擎通過解析 html文檔 構建一棵DOM樹,之后根據DOM元素的幾何屬性構建一棵用於展示渲染的渲染樹。渲染樹中的節點被稱為“幀”或“盒",符合CSS模型的定義,可理解為(包括理解頁面元素為一個具有大小,填充,邊距,邊框和位置的盒子)。由於隱藏元素不需要顯示,渲染樹中並不包含DOM樹中隱藏的元素(知道這點有用)。 當渲染樹構建完成,瀏覽器把每一個元素放到正確的位置上,然后再根據每一個元素的其他樣式,繪制頁面。
由於瀏覽器的流布局,對渲染樹的計算通常只需要遍歷一次就可以完成。但table及其內部元素除外,它可能需要多次計算才能確定好其在渲染樹中節點的屬性,通常要花3倍於同等元素的時間。這也是為什么我們要避免使用table做布局的一個原因。
重繪:是一個元素外觀的改變所觸發的瀏覽器行為,例如改變visibility、outline、背景色等屬性(上面說到的其他屬性)。瀏覽器會根據元素的新屬性重新繪制,使元素呈現新的外觀。重繪不會帶來重新布局,並不一定伴隨重排。
重排:當DOM的變化影響了元素的幾何屬性(寬或高),瀏覽器需要重新計算元素的幾何屬性,同樣其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。這個過程稱為重排。重排一定伴隨着重繪。
2. 觸發重排的操作:
2.1 修改DOM元素幾何屬性:
修改元素大小,位置,內容(一般只有重繪,但是內容可能導致元素大小變化)
2.2 DOM樹結構發生變化
當DOM樹的結構變化時,例如節點的增減、移動等,也會觸發重排。瀏覽器引擎布局的過程,類似於樹的前序遍歷,是一個從上到下從左到右的過程。 通常在這個過程中,當前元素不會再影響其前面已經遍歷過的元素。所以,如果在body最前面插入一個元素,會導致整個文檔的重新渲染,而在其后插入一個元 素,則不會影響到前面的元素。
2.4 改變瀏覽器大小
3.渲染樹變化的排隊和刷新
思考下面代碼:
1 var ele = document.getElementById('myDiv'); 2 ele.style.borderLeft = '1px'; 3 ele.style.borderRight = '2px'; 4 // var _top = ele.offsetTop; //刷新隊列 5 ele.style.padding = '5px';
三行代碼,三次修改元素的幾何屬性,瀏覽器應該發生三次重排重繪。
但是瀏覽器並不會這么笨,它也是有做優化的。它會把三次修改“保存”起來(大多數瀏覽器通過隊列化修改並批量執行來優化重排過程,也有設置時間片段的),一次完成!
然而,如果你在三行代碼中,以下獲取DOM布局信息。(為了返回最新的布局信息,將立即執行渲染樹變化隊列的更新)
如上面被注釋的第4行,如果取消注釋會導致(2+3)、(5)兩次重排;
獲取關於DOM布局信息的屬性:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle() (currentStyle in IE)
4 應對方法:盡量減少重繪次數、減少重排次數、縮小重排的影響范圍。
4.1 合並多次操作,如上面的操作
ele.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
4.2 將需要多次重排的元素,position屬性設為absolute或fixed,這樣此元素就脫離了文檔流,它的變化不會影響到其他元素。例如有動畫效果的元素就最好設置為絕對定位。
4.3 由於display屬性為none的元素不在渲染樹中,對隱藏的元素操作不會引發其他元素的重排。如果要對一個元素進行復雜的操作時,可以先隱藏它,操作完成后再顯示。這樣只在隱藏和顯示時觸發2次重排。但是這可能導致瀏覽器的閃爍。
4.4 在內存中多次操作節點,完成后再添加到文檔中去(可使用fragment元素)。例如要異步獲取表格數據,渲染到頁面。可以先取得數據后在內存中構建整個表格的html片段,再一次性添加到文檔中去,而不是循環添加每一行。
var fragment = document.createDocumentFragment(); // 未使用的虛擬節點,appendChild(fragment) //append的是里面的子元素 var li = document.createElement('li'); li.innerHTML = 'apple'; fragment.appendChild(li); var li = document.createElement('li'); li.innerHTML = 'watermelon'; fragment.appendChild(li); document.getElementById('fruit').appendChild(fragment);