題目
具體化
頁面上有個空的無序列表節點 ul ,其 id 為 list-with-big-data ,現需要往列表插入 10w 個 li ,每個列表項的文本內容可自行定義,且要求當每個 li 被單擊時,通過 alert 顯示列表項內的文本內容。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>頁面加載海量數據</title> </head> <body> <ul id="list-with-big-data">100000 數據</ul> <script> // 此處添加你的代碼邏輯 </script> </body> </html>
分析
可能在看到這個問題的第一眼,我們可能會想到這樣的解決辦法:獲取 ul 元素,然后新建 li 元素,並設置好 li 的文本內容和監聽器綁定,然后在循環里對 ul 進行 append 操作,即可能想到的是以下代碼實現。
(function() { const ulContainer = document.getElementById("list-with-big-data"); // 防御性編程 if (!ulContainer) { return; } for (let i = 0; i < 100000; i++) { const liItem = document.createElement("li"); liItem.innerText = i + 1; // EventListener 回調函數的 this 默認指向當前節點,若使用箭頭函數,得謹慎 liItem.addEventListener("click", function() { alert(this.innerText); }); ulContainer.appendChild(liItem); } })(); 復制代碼
實踐上述代碼,我們發現界面體驗很不友好,卡頓感嚴重。出現卡頓感的主要原因是,在每次循環中,都會修改 DOM 結構,並且由於數據量大,導致循環執行時間過長,瀏覽器的渲染幀率過低。
事實上,包含 100000 個 li 的長列表,用戶不會立即看到全部,只會看到少部分。因此,對於大部分的 li 的渲染工作,我們可以延時完成。
我們可以從 減少 DOM 操作次數 和 縮短循環時間 兩個方面減少主線程阻塞的時間。
DocumentFragment
The DocumentFragment interface represents a minimal document object that has no parent. It is used as a lightweight version of Document that stores a segment of a document structure comprised of nodes just like a standard document. The key difference is that because the document fragment isn't part of the active document tree structure, changes made to the fragment don't affect the document, cause reflow, or incur any performance impact that can occur when changes are made.
在 MDN 的介紹中,我們知道可以通過 DocumentFragment 的使用,減少 DOM 操作次數,降低回流對性能的影響。
requestAniminationFrame
The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.
在縮短循環時間方面,我們可以通過 分治 的思想,將 100000 個 li 分批插入到頁面中,並且我們通過 requestAniminationFrame 在頁面重繪前插入新節點。
事件綁定
如果我們想監聽海量元素,推薦方法是使用 JavaScript 的事件機制,實現事件委托,這樣可以顯著減少 DOM 事件注冊 的數量。
廣州設計公司https://www.houdianzi.com 我的007辦公資源網站https://www.wode007.com
解決方案
經過上面的討論,我們有了如下的解決方案。
(function() { const ulContainer = document.getElementById("list-with-big-data"); // 防御性編程 if (!ulContainer) { return; } const total = 100000; // 插入數據的總數 const batchSize = 4; // 每次批量插入的節點個數,個數越多,界面越卡頓 const batchCount = total / batchSize; // 批處理的次數 let batchDone = 0; // 已完成的批處理個數 function appendItems() { // 使用 DocumentFragment 減少 DOM 操作次數,對已有元素不進行回流 const fragment = document.createDocumentFragment(); for (let i = 0; i < batchSize; i++) { const liItem = document.createElement("li"); liItem.innerText = batchDone * batchSize + i + 1; fragment.appendChild(liItem); } // 每次批處理只修改 1 次 DOM ulContainer.appendChild(fragment); batchDone++; doAppendBatch(); } function doAppendBatch() { if (batchDone < batchCount) { // 在重繪之前,分批插入新節點 window.requestAnimationFrame(appendItems); } } // kickoff doAppendBatch(); // 使用 事件委托 ,利用 JavaScript 的事件機制,實現對海量元素的監聽,有效減少事件注冊的數量 ulContainer.addEventListener("click", function(e) { const target = e.target; if (target.tagName === "LI") { alert(target.innerText); } }); })();