《高性能JavaScript》讀書筆記——DOM操作

用JS進(jìn)行DOM操作的代價(jià)是昂貴的峰鄙,它是富web應(yīng)用中最常見(jiàn)的性能瓶頸。

DOM

文檔對(duì)象模型(DOM)是一個(gè)獨(dú)立于語(yǔ)言的,用于操作XML和HTML文檔的程序接口(API)谍失。通常在瀏覽器中DOM和JS都是獨(dú)立的晰筛,因?yàn)楸舜霜?dú)立嫡丙,所以JS操作DOM拴袭,性能開(kāi)銷就很大。

提升性能最佳實(shí)踐

  • 減少操作
    典型場(chǎng)景曙博,循環(huán)操作DOM拥刻,改成循環(huán)拼接,最后操作DOM父泳。
//循環(huán)操作DOM15000次
function innerHTMLLoop(){
    for(var count = 0; count < 15000; count++){
        document.getElementById('here').innerHTML += 'a';
    }
}
//只操作1次DOM般哼,在IE8中性能提升273倍
function innerHTMLLoop(){
    var content = '';
    for(var count = 0; count < 15000; count++){
        content += 'a';
    }
    document.getElementById('here').innerHTML = content;
}
  • 增加新元素時(shí)innerHTML性能高于DOM方法
var newDiv = "<div></div>";
document.getElementById('here').innerHTML = newDiv;
//
var newElement = document.createElement('div');
document.getElementById('here').appendChild(newElement);
  • 使用節(jié)點(diǎn)clone
var newElement1 = document.createElement('div');
var newElementN = newElement1.cloneNode(true);
  • HTML集合使用array代替
    DOM查詢方法(getElementByName,getElementByClassName惠窄,getElementByTagName)蒸眠,以及部分屬性(images,links,forms,elements)返回值是HTML集合(類似數(shù)組,有l(wèi)ength和下標(biāo)訪問(wèn)杆融,無(wú)push楞卡,slice)。這些集合要避免重復(fù)訪問(wèn)脾歇,因?yàn)槊看卧L問(wèn)都會(huì)重新執(zhí)行查詢蒋腮。
//由于每次添加div后,長(zhǎng)度增加介劫,此循環(huán)是死循環(huán)
var divList= document.getElementByName('div')徽惋;
for(var count = 0; count < divList.length; count++){
    document.body.appendChild(document.createElement('div'));
}
//集合保存到變量,避免重復(fù)查詢
for(var count = 0; count < document.getElementByName('div').length; count++){
    //do something
}
var divList= document.getElementByName('div')座韵;
var len = divList.length
var arr = toArray(divList); //toArray是自定義的函數(shù)险绘,將集合轉(zhuǎn)成array
for(var count = 0; count < len; count++){
    //do something
}

注意:此方法會(huì)額外增加一次遍歷操作,長(zhǎng)度小的集合可能不會(huì)提升反而下降

  • 局部變量替代
for(var i = 0; i < document.getElementsByTagName("a").length; i++){
        document.getElementsByTagName("a")[i].class = 'active'
}
//改進(jìn)后
var list = document.getElementsByTagName("a");
var len = list.length;
for(var i = 0; i < len; i++){
        list[i].class = 'active'
}
  • IE6誉碴、7中遍歷DOM宦棺,nextSibling性能高于childNode
  • 遍歷元素節(jié)點(diǎn)優(yōu)選元素節(jié)點(diǎn)屬性
    childNodes,firstChild和nextSibling這些屬性并不區(qū)分元素節(jié)點(diǎn)和其他類型節(jié)點(diǎn)(比如注釋和文本節(jié)點(diǎn))。如果只需要查詢?cè)毓?jié)點(diǎn)黔帕,優(yōu)先使用如下方法代替代咸。(注意:IE6-8只支持children屬性)
元素節(jié)點(diǎn)屬性名 被替代的屬性名
children childNodes
childElementCount childNodes.length
firstElementChild firstChild
lastElementChild lastChild
nextElementSibling nextSibling
previousElementSibling previousSibling
  • 利用CSS選擇器提高查找效率
    querySelectorAll()方法使用CSS選擇器作為參數(shù),并且返回匹配節(jié)點(diǎn)的類數(shù)組對(duì)象成黄。不會(huì)返回HTML集合呐芥,不會(huì)對(duì)應(yīng)實(shí)時(shí)的文檔結(jié)構(gòu),避免了之前討論的HTML集合引起的性能和邏輯問(wèn)題奋岁。代碼示例如下:
//改進(jìn)前
var els = document.getElementsById("menu").getElementsByTagName("a");
//改進(jìn)后
var els = document.querySelectorAll("#menu a");

如果是組合查詢思瘟,querySelectorAll()方法更具優(yōu)勢(shì)。對(duì)比一下:

//改進(jìn)前
var els = [];
var divs = document.getElementsByTagName("div");
var className = "";
for(var i = 0,len = divs.length; i<len;i++){
    className = divs[i].className;
    if(className === 'warning' || className === 'notice'){
        els.push(divs[i]);
    }
}
//改進(jìn)后
var els = document.querySelectorAll("div.warning, div.notice");

推薦使用querySelector()方法闻伶,查詢第一個(gè)匹配的節(jié)點(diǎn)滨攻。

重繪與重排

瀏覽器下載完所有的組件文件(html、js、css光绕、圖片)之后女嘲,會(huì)解析這些組件,并生成兩個(gè)數(shù)據(jù)結(jié)構(gòu):

  1. DOM樹(shù):表示頁(yè)面結(jié)構(gòu)
  2. 渲染樹(shù):表示DOM節(jié)點(diǎn)如何顯示
    DOM樹(shù)中的每一個(gè)需要顯示的節(jié)點(diǎn)在渲染樹(shù)中至少存在一個(gè)對(duì)應(yīng)的節(jié)點(diǎn)(隱藏的DOM元素在渲染樹(shù)中沒(méi)有對(duì)應(yīng)的節(jié)點(diǎn))诞帐。渲染樹(shù)中的節(jié)點(diǎn)被稱為“幀”或者“盒”欣尼,具有內(nèi)邊距padding,外邊距margin景埃,邊框border和位置position(IE盒模型的高度和寬度包括邊框和內(nèi)邊距媒至,W3C只是內(nèi)容部分。W3C盒模型可使用box-sizing:border-box改成IE盒模型)谷徙。一旦DOM樹(shù)和渲染樹(shù)構(gòu)建完成拒啰,瀏覽器就開(kāi)始顯示(繪制)頁(yè)面元素。
    當(dāng)DOM的變化影響了元素的幾何屬性(寬和高)完慧,瀏覽器需要重新計(jì)算元素的集合屬性谋旦,同事其他元素的集合屬性和位置也會(huì)受到影響。瀏覽器會(huì)使渲染樹(shù)中受到影響的部分失效屈尼,并重新構(gòu)造渲染樹(shù)册着。這個(gè)過(guò)程被稱為“重排reflow”。完成重排后脾歧,瀏覽器會(huì)重新繪制受影響的部分到平路中甲捏,該過(guò)程被稱為“重繪repaint”。
    并不是所有的DOM變化都會(huì)影響幾何屬性鞭执,比如改變背景色司顿,此時(shí)只會(huì)執(zhí)行重繪而不會(huì)觸發(fā)重排。重繪和重排都是代價(jià)昂貴的操作兄纺,需要盡量避免大溜。

觸發(fā)重排的操作

  1. 添加和刪除可見(jiàn)的DOM元素
  2. 元素位置改變
  3. 元素尺寸變化
  4. 內(nèi)容改變
  5. 頁(yè)面渲染器初始化
  6. 瀏覽器窗口尺寸改變
  7. 滾動(dòng)條的出現(xiàn)和消失會(huì)觸發(fā)整個(gè)頁(yè)面的重排

渲染樹(shù)變化的排隊(duì)與刷新

由于重排消耗大,大多數(shù)瀏覽器都會(huì)通過(guò)隊(duì)列化修改并批量執(zhí)行來(lái)優(yōu)化重排過(guò)程估脆。獲取布局的如下操作會(huì)導(dǎo)致隊(duì)列刷新:

  • offsetTop钦奋,offsetLeft,offsetWidth疙赠,offsetHeight
  • scrollTop付材,scrollLeft,scrollWidth圃阳,scrollHeight
  • clientTop伞租,clientLeft,clientWidth限佩,clientHeight
  • getCumputedStyle()(currentStyle in IE)
    執(zhí)行這些屬性和方法需要返回最新的布局信息,因此瀏覽器會(huì)執(zhí)行渲染隊(duì)列中的操作,已獲得最新的布局信息祟同。因此不需要避免頻繁執(zhí)行這些屬性和方法作喘。

最小化重排和重繪

為減少重排或者重繪,應(yīng)該合并多次對(duì)DOM和樣式的修改晕城,然后一次性處理泞坦。

修改樣式

//優(yōu)化前,執(zhí)行了三次重排。大部分現(xiàn)代瀏覽器進(jìn)行了優(yōu)化砖顷,可能只執(zhí)行一次
var el = document.getElementById('mydiv');
el.style.borderLeft='1px';
el.style.borderRight='2px';
el.style.padding='5px';
//優(yōu)化后贰锁,只執(zhí)行一次
var el = document.getElementById('mydiv');
el.style.ccsText='border-left:1px;border-right:2px;padding:5px;';
//第二種優(yōu)化方法
var el = document.getElementById('mydiv');
el.className='active';

批量修改DOM

可以通過(guò)如下步驟減少重繪和重排次數(shù):

  1. 使元素脫離文檔流
  2. 對(duì)其應(yīng)用多重改變
  3. 把元素待會(huì)文檔中
    這樣操作后只會(huì)在1和3補(bǔ)執(zhí)行兩次重排,忽略了步驟2中可能的N次重排滤蝠。
    使元素脫離文檔流的方法有如下三種:
  4. 隱藏元素豌熄,應(yīng)用修改,重新顯示
  5. 使用文檔片段在當(dāng)前DOM之外構(gòu)建一個(gè)子樹(shù)物咳,執(zhí)行完修改后再把它拷貝回文檔
  6. 講原始元素拷貝到一個(gè)脫離文檔的節(jié)點(diǎn)中锣险,修改這個(gè)副本,完成后再替換原始元素览闰。
function appendDataToElement(appendToElement, data) {
    var a, li;
    for (var i = 0, max = data.length; i < max; i++) {
        a = document.createElement('a');
        a.href = data[i].url;
        a.appendChild(document.createTextNode(data[i].name));
        li = document.createElement('li');
        li.appendChild(a);
        appendToElement.appendChild(li);
    }
};
//優(yōu)化前,循環(huán)內(nèi)N次重排
var ul = document.getElementById('mylist');
appendDataToElement(ul, data);
//第一種芯肤,異常和顯示
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
//第二種,文檔片段
var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);
//第三種压鉴,元素替換
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

推薦第二種方案崖咨,其產(chǎn)生的DOM遍歷和重排次數(shù)最少。

緩存布局信息

當(dāng)查詢布局信息(例如offsets,scroll,client等)油吭,瀏覽器為返回最新值击蹲,會(huì)刷新隊(duì)列并應(yīng)用所有變更。所喲盡量減少布局信息的獲取次數(shù)上鞠,獲取后賦值給局部變量际邻,然后再操作局部變量。
例如移動(dòng)元素的例子芍阎,timeout循環(huán)部分

//改進(jìn)前
myElement.style.left = 1 + myElement.offsetLeft + 'px';
myElement.style.top = 1 + myElement.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
    stopAnimation();
}
//改進(jìn)后世曾,先一次性獲取初始位置
var current = myElement.offsetLeft;
//然后循環(huán)執(zhí)行操作
current++
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) {
    stopAnimation();
}

讓元素脫離動(dòng)畫流

采用絕對(duì)位置定位,可以減少元素尺寸變化時(shí)谴咸,對(duì)其他元素造成的重排影響轮听。
例如折疊/展開(kāi)這種交互方式,每次變化都會(huì)導(dǎo)致下方所有元素的移動(dòng)岭佳。如果把這部分元素使用絕對(duì)位置定位血巍,覆蓋其他部分。這樣就能避免下方元素的重排和重繪珊随,減少開(kāi)銷述寡。

IE和:hover

從IE7開(kāi)始柿隙,IE允許任何元素上使用:hover這個(gè)CSS偽選擇器。如果大量使用:hover鲫凶,響應(yīng)速度下降明顯禀崖。特別是IE8。

事件委托

如果進(jìn)行大量的DOM元素事件綁定螟炫,會(huì)引入性能問(wèn)題波附。一個(gè)簡(jiǎn)單的解決方案是事件委托。只需給最外層的元素綁定事件昼钻,利用事件逐層冒泡并能被父級(jí)元素捕獲掸屡,就可以處理所有子元素上觸發(fā)的事件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末然评,一起剝皮案震驚了整個(gè)濱河市仅财,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沾瓦,老刑警劉巖满着,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異贯莺,居然都是意外死亡风喇,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門缕探,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)魂莫,“玉大人,你說(shuō)我怎么就攤上這事爹耗“铱迹” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵潭兽,是天一觀的道長(zhǎng)倦始。 經(jīng)常有香客問(wèn)我,道長(zhǎng)山卦,這世上最難降的妖魔是什么鞋邑? 我笑而不...
    開(kāi)封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮账蓉,結(jié)果婚禮上枚碗,老公的妹妹穿的比我還像新娘。我一直安慰自己铸本,他們只是感情好肮雨,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著箱玷,像睡著了一般怨规。 火紅的嫁衣襯著肌膚如雪陌宿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天椅亚,我揣著相機(jī)與錄音限番,去河邊找鬼。 笑死呀舔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扩灯。 我是一名探鬼主播媚赖,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼珠插!你這毒婦竟也來(lái)了惧磺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捻撑,失蹤者是張志新(化名)和其女友劉穎磨隘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體顾患,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡番捂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了江解。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片设预。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖犁河,靈堂內(nèi)的尸體忽然破棺而出鳖枕,到底是詐尸還是另有隱情,我是刑警寧澤桨螺,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布宾符,位于F島的核電站,受9級(jí)特大地震影響灭翔,放射性物質(zhì)發(fā)生泄漏魏烫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一缠局、第九天 我趴在偏房一處隱蔽的房頂上張望则奥。 院中可真熱鬧,春花似錦狭园、人聲如沸读处。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)罚舱。三九已至井辜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間管闷,已是汗流浹背粥脚。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留包个,地道東北人刷允。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像碧囊,于是被迫代替她去往敵國(guó)和親树灶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容