摘要:當(dāng)遍歷一個(gè)集合時(shí),首要優(yōu)化原則是把集合存儲在局部變量中,并把緩存在循環(huán)外部,然后使用局部變量訪問這些需要多次訪問的元素。
友情提醒:這篇我都覺得有點(diǎn)長...可能會占用你10+分鐘,沒有這么多時(shí)間的你可以直接去文末看小姐結(jié)。
瀏覽器中的DOM用腳本進(jìn)行DOM操作的代價(jià)是很昂貴的,它是富web應(yīng)用中最常見的性能瓶頸。主要有以下三種問題:
訪問和修改DOM元素
修改DOM元素的樣式導(dǎo)致repaint和reflow
通過DOM事件處理與用戶進(jìn)行交互
DOM是(Document Object Model)一個(gè)與語言無關(guān)的、用來操作XML和HTML文檔的應(yīng)用程序接口(Application Program Interface)。 盡管DOM與語言無關(guān),但是在瀏覽器中的接口卻是用JavaScript來實(shí)現(xiàn)的。
一個(gè)前端小知識瀏覽器通常會把js和DOM分開來分別獨(dú)立實(shí)現(xiàn)。
舉個(gè)栗子冷知識,在IE中,js的實(shí)現(xiàn)名為JScript,位于jscript.dll文件中;DOM的實(shí)現(xiàn)則存在另一個(gè)庫中,名為mshtml.dll(Trident)。
Chrome中的DOM實(shí)現(xiàn)為webkit中的webCore,但js引擎是Google自己研發(fā)的V8。
Firefox中的js引擎是SpiderMonkey,渲染引擎(DOM)則是Gecko。
前面的小知識中說過,瀏覽器把實(shí)現(xiàn)頁面渲染的部分和解析js的部分分開來實(shí)現(xiàn),既然是分開的,一旦兩者需要產(chǎn)生連接,就要付出代價(jià)。
兩個(gè)例子:
小明和小紅是兩個(gè)不同學(xué)校的學(xué)生,兩個(gè)人家里經(jīng)濟(jì)條件都不太好,買不起手機(jī)(好尷尬的設(shè)定Orz...),所以只能通過寫信來互相交流,這樣的過程肯定比他倆面對面交談時(shí)所需要花費(fèi)的代價(jià)大(額外的事件、寫信的成本等)。
官方例子:把DOM和js(ECMAScript)各自想象為一座島嶼,它們之間用收費(fèi)橋進(jìn)行連接。ECMAScript每次訪問DOM,都要途徑這座橋,并交納“過橋費(fèi)”。訪問DOM的次數(shù)越多,費(fèi)用也就越高。
因此,推薦的做法是:盡可能的減少過橋的次數(shù),努力待在ECMAScript島上。
DOM的訪問與修改前面說到訪問DOM需要交納“過橋費(fèi)”,而修改DOM元素則代價(jià)更為昂貴,因?yàn)樗鼤?dǎo)致瀏覽器重新計(jì)算頁面的幾何變化。
來看一段代碼:
function innerHTMLLoop(){ for (var count = 0; count < 15000; count++){ document.getElementById("text").innerHTML += "dom"; } }
這段代碼,每次循環(huán)會訪問兩次特定的元素:第一次讀取這個(gè)元素的innerHTML屬性,第二次重寫它。
看清楚了這一點(diǎn),不難得到一個(gè)效率更高的版本:
function innerHTMLLoop2(){ var content = ""; for (var count = 0; count < 15000; count++){ content += "dom"; } document.getElementById("text").innerHTML += content; }
用一個(gè)局部變量包層每次更新后的內(nèi)容,等待循環(huán)結(jié)束后,一次性的寫入頁面(盡可能的把更多的工作交給js的部分來做)。
根據(jù)統(tǒng)計(jì),在所有的瀏覽器中,修改后的版本都運(yùn)行的更快(優(yōu)化幅度最明顯的是IE8,使用后者比使用前者快273倍)。
HTML元素集合是包含了DOM節(jié)點(diǎn)引用的類數(shù)組對象。
可以用以下方法或?qū)傩缘玫揭粋€(gè)HTML元素集合:
document.getElementsByName()
document.getElementsByTagName()
document.getElementsByClassName()
document.images 頁面中所有img元素
document.links 頁面中所有a元素
document.forms 頁面中所有表單元素
document.forms[0].elements 頁面中第一個(gè)表單的所有字段
HTML元素集合處于一種“實(shí)時(shí)的狀態(tài)”,這意味著當(dāng)?shù)讓游臋n對象更新時(shí),它也會自動更新,也就是說,HTML元素集合與底層的文檔對象之間保持的連接。正因如此,每當(dāng)你想從HTML元素集合中獲取一些信息時(shí),都會產(chǎn)生一次查詢操作,這正是低效之源。
昂貴的集合//這是一個(gè)死循環(huán) //不管你信不信,反正我是信了 var alldivs = document.getElementsByTagName("div"); for (var i = 0; i < alldivs.length; i++){ document.body.appendChild(document.createElement("div")); }
乍一看,這段代碼只是單純的把頁面中的div數(shù)量翻倍:遍歷所有的div,每次創(chuàng)建一個(gè)新的div并創(chuàng)建到添加到body中。
但事實(shí)上,這是一個(gè)死循環(huán):因?yàn)檠h(huán)的退出條件alldivs.length在每一次循環(huán)結(jié)束后都會增加,因?yàn)檫@個(gè)HTML元素集合反映的是底層文檔元素的實(shí)時(shí)狀態(tài)。
接下來,我們通過這段代碼,對一個(gè)HTML元素集合做一些處理:
function toArray(coll){ for (var i = 0, a = [], len = coll.lengthl i < len; i++){ a[i] = coll[i]; } return a; } //將一個(gè)HTML元素集合拷貝到一個(gè)數(shù)組中 var coll = document.getElementsByTagName("div"); var arr = toArray(coll);
現(xiàn)在比較以下兩個(gè)函數(shù):
function loopCollection(){ for (var count = 0; count < coll.length; count++){ //processing... } } function loopCopiedArray(){ for (var count = 0; count < arr.length; count++){ //processing... } }
在IE6中,后者比前者快114倍;IE7中119倍;IE8中79倍...
所以,在相同的內(nèi)容和數(shù)量下,遍歷一個(gè)數(shù)組的速度明顯快于遍歷一個(gè)HTML元素集合。
由于在每一次迭代循環(huán)中,讀取元素集合的length屬性會引發(fā)集合進(jìn)行更新,這在所有的瀏覽器中都有明顯的性能問題,所以你也可以這么干:
function loopCacheLengthCollection(){ var coll = document.getElementsByTagName("div"), len = coll.length; for (var count = 0; count < len; count++){ //processing... } }
這個(gè)函數(shù)和上面的loopCopiedArray()一樣快。
訪問集合元素時(shí)使用局部變量一般來說,對于任何類型的DOM訪問,當(dāng)同一個(gè)DOM屬性或者方法需要被多次訪問時(shí),最好使用一個(gè)局部變量緩存此成員。當(dāng)遍歷一個(gè)集合時(shí),首要優(yōu)化原則是把集合存儲在局部變量中,并把length緩存在循環(huán)外部,然后使用局部變量訪問這些需要多次訪問的元素。
一個(gè)栗子,在循環(huán)之中訪問每個(gè)元素的三個(gè)屬性。
function collectionGlobal(){ var coll = document.getElementsByTagName("div"), len = coll.length, name = ""; for (var count = 0; count < len; count++){ name = document.getElementsByTagName("div")[count].nodeName; name = document.getElementsByTagName("div")[count].nodeType; name = document.getElementsByTagName("div")[count].tagName; //我的天不會有人真的這么寫吧... } return name; }
上面這段代碼,大家不要當(dāng)真...正常人肯定是寫不出來的...這里是為了對比一下,所以把這種最慢的情況寫給大家看。
接下來,是一個(gè)稍微優(yōu)化了的版本:
function collectionLocal(){ var coll = document.getElementsByTagName("div"), len = coll.length, name = ""; for (var count = 0; count < length; count++){ name = coll[count].nodeName; name = coll[count].nodeType; name = coll[count].tagName; } return name; }
這次就看起來正常很多了,最后是這次優(yōu)化之旅的最終版本:
function collectionNodesLocal(){ var coll = document.getElementsByTagName("div"), len = coll.length, name = "", ele = null; for (var count = 0; count < len; count++){ ele = coll[count]; name = ele.nodeName; name = ele.nodeType; name = ele.tagName; } return name; }遍歷DOM 在DOM中爬行
通常你需要從某一個(gè)DOM元素開始,操作周圍的元素,或者遞歸查找所有的子節(jié)點(diǎn)。
考慮下面兩個(gè)等價(jià)的栗子:
//1 function testNextSibling(){ var el = document.getElementById("mydiv"), ch = el.firstChild, name = ""; do { name = ch.nodeName; } while (ch = ch.nextSibling); return name; } //2 function testChildNodes(){ var el = document.getElementById("mydiv"), ch = el.childNodes, len = ch.length, //childNodes是一個(gè)元素集合,因此在循環(huán)中主席緩存length屬性以避免迭代更新 name = ""; for (var count = 0; count < len; count++){ name = ch[count].nodeName; } return name; }
在不同瀏覽器中,兩種方法的運(yùn)行時(shí)間幾乎相等。但在老版本的IE瀏覽器中,nextSibling的性能比childNodes更好一些。
元素節(jié)點(diǎn)我們知道,DOM節(jié)點(diǎn)有以下五種分類:
整個(gè)文檔是一個(gè)文檔節(jié)點(diǎn)
每個(gè)HTML元素是元素節(jié)點(diǎn)
HTML元素內(nèi)的文本是文本節(jié)點(diǎn)
每個(gè)HTML屬性是屬性節(jié)點(diǎn)
注釋是注釋節(jié)點(diǎn)
諸如childNodes、firstChild、nextSibling這些DOM屬性是不區(qū)分元素節(jié)點(diǎn)和其他類型的節(jié)點(diǎn)的,但往往我們只需要訪問元素節(jié)點(diǎn),此時(shí)需要做一些過濾的工作。事實(shí)上,這些類型檢查的過程都是不必要的DOM操作。
許多現(xiàn)代瀏覽器提供的API只返回元素節(jié)點(diǎn),如果可用的話推薦直接只用這些API,因?yàn)樗鼈兊膱?zhí)行效率比自己在js中過濾的效率要高。
現(xiàn)代瀏覽器提供的API(被替換的API)
children(childNodes)
childElementCount (childNodes.length)
firstElementChild (firstChild)
lastElementChild (lastChild)
nextElementSibling (nextSibling)
previousElementSibling (previousSibling)
使用這些新的API,可以直接獲取到元素節(jié)點(diǎn),也正是因此,其速度也更快。
選擇器API有時(shí)候?yàn)榱说玫叫枰脑亓斜恚_發(fā)人員不得不組合調(diào)用getElementById、getElementsByTagName,并遍歷返回的節(jié)點(diǎn),但這種繁密的過程效率低下。
最新的瀏覽器提供了一個(gè)傳遞參數(shù)為CSS選擇器的名為querySelectorAll()的原生DOM方法。這種方式自然比使用js和DOM來遍歷查找元素要快的多。
比如,
var elements = document.querySelectorAll("#menu a");
這一段代碼,返回的是一個(gè)NodeList————包含著匹配節(jié)點(diǎn)的類數(shù)組對象。與之前不同的是,這個(gè)方法不會返回HTML元素集合,因此返回的節(jié)點(diǎn)不會對應(yīng)實(shí)時(shí)的文檔結(jié)構(gòu),也避免了之前由于HTML集合引起的性能(潛在邏輯)問題。
如果不使用querySelectorAll(),我們需要這樣寫:
var elements = document.getElementById("menu").getElementsByTagName("a");
不僅寫起來更麻煩了,更要注意的是,此時(shí)的elements是一個(gè)HTML元素集合,所以還需要把它c(diǎn)opy到數(shù)組中,才能得到一個(gè)與前者相似的靜態(tài)列表。
還有一個(gè)querySelector()方法,用來獲取第一個(gè)匹配的節(jié)點(diǎn)。
瀏覽器用來顯示頁面的所有“組件”,有:HTML標(biāo)簽、js、css、圖片——之后會解析并生成兩個(gè)內(nèi)部的數(shù)據(jù)結(jié)構(gòu):
DOM樹(表示頁面結(jié)構(gòu))
渲染樹(表示DOM節(jié)點(diǎn)應(yīng)該如何表示)
DOM樹中的每一個(gè)需要顯示的節(jié)點(diǎn)在渲染樹中至少存在一個(gè)對應(yīng)的節(jié)點(diǎn)。
渲染樹中的節(jié)點(diǎn)被稱為“幀(frames)”或“盒(boxes)”,符合css盒模型的定義,理解頁面元素為一個(gè)具有padding、margin、borders和position的盒子。
一旦渲染樹構(gòu)建完成,瀏覽器就開始顯示頁面元素,這個(gè)過程稱為繪制(paint)。
當(dāng)DOM的變化影響了元素的幾何屬性(寬、高)——比如改變改變了邊框的寬度或者給一個(gè)段落增加一些文字導(dǎo)致其行數(shù)的增加——瀏覽器就需要重新計(jì)算元素的幾何屬性,同樣,頁面中其他元素的幾何屬性和位置也會因此受到影響。
瀏覽器會使渲染樹中收到影響的部分消失,重新構(gòu)建渲染樹,這個(gè)過程稱為“重排(reflow)”。重排完成之后,瀏覽器會重新將受到影響的部分繪制到瀏覽器中,這個(gè)過程稱之為“重繪(repaint)”。
如果改變的不是元素的幾何屬性,如:改變元素的背景顏色,不會發(fā)生重排,只會發(fā)生一次重繪,因?yàn)樵氐牟季植]有改變。
不管是重繪還是重排,都是代價(jià)昂貴的操作,它們會導(dǎo)致web應(yīng)用程序的UI反應(yīng)遲鈍,應(yīng)當(dāng)盡可能的減少這類過程的發(fā)生。
添加或刪除可見的DOM元素
元素位置的改變
元素尺寸的改變(padding、margin、border、height、width)
內(nèi)容改變(文本改變或圖片尺寸改變)
頁面渲染器初始化
瀏覽器窗口尺寸改變
滾動條的出現(xiàn)(會觸發(fā)整個(gè)頁面的重排)
最小化重繪和重排 改變樣式一個(gè)栗子:
var el = document.getElementById("mydiv"); el.style.borderLeft = "1px"; el.style.borderRight = "2px"; el.style.padding = "5px";
示例中,元素的三個(gè)樣式被改變,而且每一個(gè)都會影響元素的幾何結(jié)構(gòu)。在最糟糕的情況下,這段代碼會觸發(fā)三次重排(大部分現(xiàn)代瀏覽器為此做了優(yōu)化,只會觸發(fā)一次重排)。從另一個(gè)角度看,這段代碼四次訪問DOM,可以被優(yōu)化。
var el = document.getElementById("mydiv"); //思路:合并所有改變?nèi)缓笠淮涡蕴幚? //method_1:使用cssText屬性 el.style.cssText = "border-left: 1px; border-right: 2px; padding: 5px"; //method_2:修改類名 el.className = "anotherClass";批量修改DOM
當(dāng)你需要對DOM元素進(jìn)行一系列操作的時(shí)候,不妨按照如下步驟:
使元素脫離文檔流
對其應(yīng)用多重改變
把元素帶回文檔中
上面的這一套組合拳中,第一步和第三部分別會觸發(fā)一次重排。但是如果你忽略了這兩個(gè)步驟,那么在第二步所產(chǎn)生的任何修改都會觸發(fā)一次重排。
在此安利三種可以使DOM元素脫離文檔流的方法:
隱藏元素
使用文檔片段(document fragment)在當(dāng)前DOM之外構(gòu)建一個(gè)子樹,再把它拷貝回文檔
將原始元素拷貝到一個(gè)脫離文檔的節(jié)點(diǎn)中,修改副本,完成后再替換原始元素
讓動畫元素脫離文檔流一般情況下,重排只影響渲染樹中的一小部分,但也可能影響很大的一部分,甚至是整個(gè)渲染樹。
瀏覽器所需的重排次數(shù)越少,應(yīng)用程序的響應(yīng)速度也就越快。
想象這樣一種情況,頁面的底部有一個(gè)動畫,會推移頁面整個(gè)余下的部分,這將是一次代價(jià)昂貴的大規(guī)模重排!用戶也勢必會感覺到頁面一卡一卡的。
因此,使用以下步驟可以避免頁面中的大部分重排:
使用絕對定位讓頁面上的動畫元素脫離文檔流
動畫展示階段
動畫結(jié)束時(shí),將元素恢復(fù)定位。
IE的:hover從IE7開始,IE允許在任何元素上使用:hover這個(gè)css選擇器。
然而,如果你有大量元素使用了:hover,你會發(fā)現(xiàn),賊喇慢!
這一個(gè)優(yōu)化手段也是在前端求職面試中的高頻題目。
當(dāng)頁面中有大量的元素,并且這些元素都需要綁定事件處理器。
每綁定一個(gè)事件處理器都是有代價(jià)的,要么加重了頁面負(fù)擔(dān),要么增加了運(yùn)行期的執(zhí)行時(shí)間。再者,事件綁定會占用處理時(shí)間,而且瀏覽器需要跟蹤每個(gè)事件處理器,這也會占用更多的內(nèi)存。還有一種情況就是,當(dāng)這些工作結(jié)束時(shí),這些事件處理器中的絕大多數(shù)都是不再需要的(并不是100%的按鈕或鏈接都會被用戶點(diǎn)擊),因此有很多工作是沒有必要的。
事件委托的原理很簡單——事件逐層冒泡并能被父級元素捕獲。
使用事件委托,只需要給外層元素綁定一個(gè)處理器,就可以處理在其子元素上觸發(fā)的所有事件。
有以下幾點(diǎn)需要注意:
訪問事件對象,判斷事件源
按需取消文檔樹中的冒泡
按需阻止默認(rèn)動作
小結(jié)訪問和操作DOM需要穿越連接ECMAScript和DOM兩個(gè)島嶼之間的橋梁,為了盡可能的減少“過橋費(fèi)”,有以下幾點(diǎn)需要注意:
最小化DOM訪問次數(shù)
對于需要多次訪問的DOM節(jié)點(diǎn),使用局部變量存儲其引用
如果要操作一個(gè)HTML元素集合,建議把它拷貝到一個(gè)數(shù)組中
使用速度更快的API:比如querySelectorAll
留意重排和重繪的次數(shù)
事件委托
分享前端和一些有趣的東西
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/81420.html
摘要:語句會使得局部變量位于作用域第二層,會使性能下降,所以應(yīng)避免使用。使用事件委托來減少事件處理器的數(shù)量。把最可能出現(xiàn)的條件放在首位。在進(jìn)行優(yōu)化時(shí),要弄清楚性能瓶頸,然后對癥優(yōu)化。新看到一篇很棒的文章前端性能優(yōu)化備忘錄如有不對,歡迎指正。 春節(jié)在家,把《高性能的JavaScript》刷了一遍,受益匪淺。本著每看完一本書都要做讀書筆記的習(xí)慣,將書中的知識點(diǎn)總結(jié)一下。 由于不同瀏覽器使用的Ja...
摘要:或者說一直以來我是缺乏開發(fā)高性能網(wǎng)頁的意識的,但是想做一個(gè)好的前端開發(fā)者,是需要在當(dāng)自己編寫的程序慢慢復(fù)雜以后還能繼續(xù)保持網(wǎng)頁的高性能的。 不知道有多少人和我一樣,在以前的開發(fā)過程中很少在乎自己編寫的網(wǎng)頁的性能。或者說一直以來我是缺乏開發(fā)高性能網(wǎng)頁的意識的,但是想做一個(gè)好的前端開發(fā)者,是需要在當(dāng)自己編寫的程序慢慢復(fù)雜以后還能繼續(xù)保持網(wǎng)頁的高性能的。這需要我們對JavaScript語句,...
摘要:性能訪問字面量和局部變量的速度是最快的,訪問數(shù)組和對象成員相對較慢變量標(biāo)識符解析過程搜索執(zhí)行環(huán)境的作用域鏈,查找同名標(biāo)識符。建議將全局變量存儲到局部變量,加快讀寫速度。優(yōu)化建議將常用的跨作用域變量存儲到局部變量,然后直接訪問局部變量。 缺陷 這本書是2010年出版的,這本書談性能是有時(shí)效性的,現(xiàn)在馬上就2018年了,這幾年前端發(fā)展的速度是飛快的,書里面還有一些內(nèi)容考慮IE6、7、8的東...
摘要:最近在全力整理高性能的文檔,并重新學(xué)習(xí)一遍,放在這里方便大家查看并找到自己需要的知識點(diǎn)。 最近在全力整理《高性能JavaScript》的文檔,并重新學(xué)習(xí)一遍,放在這里方便大家查看并找到自己需要的知識點(diǎn)。 前端開發(fā)文檔 高性能JavaScript 第1章:加載和執(zhí)行 腳本位置 阻止腳本 無阻塞的腳本 延遲的腳本 動態(tài)腳本元素 XMLHTTPRequest腳本注入 推薦的無阻塞模式...
摘要:事件冒泡一個(gè)簡單,但是坑了我無數(shù)回的知識點(diǎn)與的交互通過事件來實(shí)現(xiàn)。而瀏覽器的事件流是一個(gè)非常重要的概念。不去討論那些古老的瀏覽器有事件捕獲與事件冒泡的爭議,只需要知道在中規(guī)定的事件流包括了三個(gè)部分,事件捕獲階段處于目標(biāo)階段事件冒泡階段。 打算封裝一個(gè)彈窗組件,做的時(shí)候忘記了考慮事件冒泡的因素,結(jié)果被坑得不要不要的。為了解決自己的問題,去查閱了不少資料,把事件流相關(guān)的知識都給總結(jié)一下。 ...
閱讀 1311·2021-11-24 10:24
閱讀 4090·2021-11-22 15:29
閱讀 1085·2019-08-30 15:53
閱讀 2788·2019-08-30 10:54
閱讀 1977·2019-08-29 17:26
閱讀 1271·2019-08-29 17:08
閱讀 605·2019-08-28 17:55
閱讀 1576·2019-08-26 14:01