加載和執(zhí)行
腳本位置
放在<head>中的javascript文件會阻塞頁面渲染:一般來說瀏覽器中有多種線程:UI渲染線程赡若、javascript引擎線程厨剪、瀏覽器事件觸發(fā)線程灵份、HTTP請求線程等。多線程之間會共享運行資源牛欢,瀏覽器的js會操作dom林束,影響渲染,所以js引擎線程和UI渲染線程是互斥的咨察,導致執(zhí)行js時會阻塞頁面的渲染。
最佳實踐:所有的Script標簽盡可能放在body標簽底部福青,以盡量減少對整個頁面下載的影響
組織腳本
每個<script>
標簽初始下載時都會阻塞頁面渲染摄狱,所以應減少頁面包含的<script>
標簽數量。內嵌腳本放在引用外鏈樣式表的<link>
標簽之后會導致頁面阻塞去等待樣式表的下載无午,建議不要把內嵌腳本緊跟在<link>
標簽之后媒役。外鏈javascript的HTTP請求還會帶來額外的性能開銷,減少腳本文件的數量將會改善性能宪迟。
無阻塞的腳本
無阻塞腳本的意義在于在頁面加載完成后才加載JavaScript代碼酣衷。(windows對象的load事件觸發(fā)后)
延遲的腳本
帶有defer屬性的<script>
標簽可以放置在文檔的任何位置。對應的Javascript文件將在頁面解析到<script>
標簽時開始下載次泽,但是并不會執(zhí)行穿仪,知道DOM加載完成(onload事件被觸發(fā)前)。當一個帶有defer屬性的JavaScript文件下載時箕憾,它將不會阻塞瀏覽器的其他進程牡借,可以與其他資源并行下載拳昌。執(zhí)行的順序是script袭异、defer、load炬藤。
動態(tài)腳本元素
使用JavaScript動態(tài)創(chuàng)建HTML中script元素御铃,例如一些懶加載庫。
優(yōu)點:動態(tài)腳本加載憑借著它在跨瀏覽器兼容性和易用的優(yōu)勢沈矿,成為最通用的無阻塞加載解決方式上真。
XHR腳本注入
創(chuàng)建XHR對象铛碑,用它下載JavaScript文件渠鸽,通過動態(tài)創(chuàng)建script元素余境,將代買注入頁面中
var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
if(xht.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
優(yōu)點:可以下載JavaScript但不立即執(zhí)行,在所有主流瀏覽器中都可以正常工作迄薄。
缺點:JavaScript文件必須與所請求的頁面處于相同的域,意味著文件從CDN下載补履。
數據存取
存儲的位置
數據存儲的位置會很大程度上影響讀取速度瓷耙。
- 字面量:字面量只代表自身,不存儲在特定的位置妻怎。包括:字符串壳炎、數字、布爾值逼侦、對象匿辩、數組、函數榛丢、正則表達式铲球、null、undefined晰赞。(個人理解:對象的指針本身是字面量)睬辐。
- 本地變量:var定義的數據存儲單元。
- 數組元素:存儲在JavaScript數組內部宾肺,以數字為引溯饵。
- 對象成員:存儲在JavaScript對象內部,以字符串作為索引锨用。
大多數情況下從一個字面量和一個局部變量中存取數據的差異是微不足道的丰刊。訪問數據元素和對象成員的代價則搞一點。如果在乎運行速度增拥,盡量使用字面量和局部變量啄巧,減少數組和對象成員的使用。
管理作用域
作用域鏈
每個JavaScript函數都表示為一個對象掌栅,更確切得說是Function對象的一個實例秩仆。它也有僅供JavaScript引擎存儲的內部屬性,其中一個內部屬性是[[Scope]]
猾封,包涵了一個被創(chuàng)建的作用域中對象的集合澄耍,即作用域鏈。作用雨量決定哪些數據能被函數訪問晌缘。作用域中的每個對象被稱為一個可變對象齐莲。
當一個函數被創(chuàng)建后,作用域鏈會被創(chuàng)建函數的作用域中可訪問的數據對象所填充磷箕。執(zhí)行函數時會創(chuàng)建一個稱為執(zhí)行上下文的內部對象选酗。執(zhí)行上下文定義了函數執(zhí)行時的環(huán)境。每次函數執(zhí)行時對應的執(zhí)行環(huán)境都是獨一無二的岳枷,多次調用同一個函數也會創(chuàng)建多個執(zhí)行上下文芒填,當函數執(zhí)行完畢呜叫,執(zhí)行上下文會被銷毀。每個執(zhí)行上下文都有自己的作用域鏈殿衰,用于解析標識符怀偷。當執(zhí)行上下文被創(chuàng)建時,它的作用域鏈初始化成當前運行函數的[[scope]]
屬性中的對象播玖。這些值哪找它們出現在函數中的順序椎工,被復制到執(zhí)行環(huán)境的作用域鏈中。這個過程一旦完成蜀踏,一個被稱為活動對象的新對象就為執(zhí)行上下文創(chuàng)建好了维蒙。
活動對象作為函數運行時的變量對象,包含了所有局部對象果覆,命名函數颅痊,參數集合以及this
。然后次對象被推入作用域鏈的最前端局待。當執(zhí)行環(huán)境被銷毀時斑响,活動對象也隨之銷毀。執(zhí)行過程中每遇到一個變量钳榨,都會經歷一次標識符解析過程以決定從哪里獲取或存儲數據舰罚。該過程搜索執(zhí)行環(huán)境的作用域鏈,查找同名的標識符薛耻。搜索過程中從作用域鏈頭部開始营罢,也就是當前運行函數的活動對象。如果找到饼齿,就是用這個標識符對應的變量饲漾,如果沒找到,繼續(xù)搜索作用域鏈的下一個對象直到找到缕溉,若無法搜索到匹配的對象考传,則標識符被當做未定義的。這個搜索過程影響了性能证鸥。
標識符解析的性能
一個標識符所在的位置越深僚楞,讀寫速度就越慢,全局變量總是存在于執(zhí)行環(huán)境作用域的最末端敌土,因此它是最深的镜硕。
最佳實踐:如果某個跨作用域的值在函數中被引用一次以上运翼,那么就把它存儲到局部變量中返干。
改變作用域鏈
一般來說一個執(zhí)行上下文的作用域鏈是不會改變的。但是血淌,with
語句和try-catch
語句的catch
子語句可以改變作用域鏈矩欠。
with
語句用來給對象的所有屬性創(chuàng)建一個變量财剖,可以避免多次書寫。但是存在性能問題:代碼執(zhí)行到with
語句時癌淮,執(zhí)行環(huán)境的作用域鏈臨時被改變了躺坟,創(chuàng)建了一個新的(包含了with對象所有屬性)對象被創(chuàng)建了,之前所有的局部變量現在處于第二個作用域鏈對象中乳蓄,提高了訪問的代價咪橙。建議放棄使用with
語句。
try-catch
語句中的catch
子句也可以改變作用域鏈虚倒,當try
代碼塊中發(fā)生錯誤美侦,執(zhí)行過程會自動跳轉到catch
子句,把異常對象推入一個變量對象并置于作用域的首位魂奥,局部變量處于第二個作用域鏈對象中菠剩。簡化代碼可以使catch
子句對性能的影響降低。
最佳實踐:將錯誤委托給一個函數來處理耻煤。
動態(tài)作用域
無論with
語句還是try-catch
語句的子句catch
子句具壮、eval()
語句,都被認為是動態(tài)作用域哈蝇。經過優(yōu)化的JavaScript引擎棺妓,嘗試通過分析代碼來確定哪些變量是可以在特定的時候被訪問,避開了傳統(tǒng)的作用域鏈炮赦,取代以標識符索引的方式快速查找涧郊。當涉及動態(tài)作用域時,這種優(yōu)化方式就失效了眼五。
最佳實踐:只在確實有必要時使用動態(tài)作用域妆艘。
閉包、作用域和內存
由于閉包的[[Scope]]
屬性包含了與執(zhí)行上下文作用域鏈相同的對象的引用看幼,因此會產生副作用批旺。通常來說,函數的活動對象會隨著執(zhí)行環(huán)境一同銷毀诵姜。但引入閉包時汽煮,由于引用仍然存在閉包的[[Scope]]
屬性中,因此激活對象無法被銷毀棚唆,導致更多的內存開銷暇赤。
最需要關注的性能點:閉包頻繁訪問跨作用域的標識符,每次訪問都會帶來性能損失宵凌。
最佳實踐:將常用的跨作用域變量存儲在局部變量中鞋囊,然后直接訪問局部變量。
對象成員
無論是通過創(chuàng)建自定義對象還是使用內置對象都會導致頻繁的訪問對象成員瞎惫。
原型
JavaScript中的對象是基于原型的溜腐。解析對象成員的過程與解析變量十分相似译株,會從對象的實例開始,如果實例中沒有挺益,會一直沿著原型鏈向上搜索歉糜,直到找到或者到原型鏈的盡頭。對象在原型鏈中位置越深望众,找到它也就越慢匪补。搜索實例成員比從字面量或局部變量中讀取數據代價更高,再加上遍歷原型鏈帶來的開銷烂翰,這讓性能問題更為嚴重叉袍。
嵌套成員
對象成員可能包含其他成員,每次遇到點操作符"."會導致JavaScript引擎搜索所有對象成員刽酱。
緩存對象成員值
由于所有類似的性能問題都與對象成員有關喳逛,因此應該盡可能避免使用他們,只在必要時使用對象成員棵里,例如润文,在同一個函數中沒有必要多次讀取同一個對象屬性(保存到局部變量中),除非它的值變了殿怜。這種方法不推薦用于對象的方法典蝌,因為將對象方法保存在局部變量中會導致this
綁定到window
,導致JavaScript引擎無法正確的解析它的對象成員头谜,進而導致程序出錯骏掀。
DOM編程
瀏覽器中的DOM
文檔對象模型(DOM)是一個獨立于語言的,用于操作XML和HTML文檔的程序接口API柱告。DOM是個與語言無關的API截驮,在瀏覽器中的接口是用JavaScript實現的〖识龋客戶端腳本編程大多數時候是在和底層文檔打交道葵袭,DOM就成為現在JavaScript編碼中的重要組成部分。瀏覽器把DOM和JavaScript單獨實現乖菱,使用不同的引擎坡锡。
天生就慢
DOM和javascript就像兩個島嶼通過收費橋梁連接,每次通過都要繳納“過橋費”窒所。
推薦的做法是盡可能減少過橋的次數鹉勒,努力待在ECMAScript島上。
DOM訪問與修改
訪問DOM元素是有代價的——前面的提到的“過橋費”吵取。修改元素則更為昂貴禽额,因為它會導致瀏覽器重新計算頁面的幾何變化(重排)。最壞的情況是在循環(huán)中訪問或修改元素海渊,尤其是對HTML元素集合循環(huán)操作绵疲。
在循環(huán)訪問頁面元素的內容時哲鸳,最佳實踐是用局部變量存儲修改中的內容臣疑,在循環(huán)結束后一次性寫入盔憨。
通用的經驗法則是:減少訪問DOM的次數,把運算盡量留在ECMAScript中處理讯沈。
節(jié)點克隆
大多數瀏覽器中使用節(jié)點克隆都比創(chuàng)建新元素要更有效率郁岩。
選擇API
使用css
選擇器也是一種定位節(jié)點的便利途徑,瀏覽器提供了一個名為querySelectorAll()
的原生DOM方法缺狠。這種方法比使用JavaScript和DOM來遍歷查找元素快很多问慎。使用另一個便利方法——querySelector()
來獲取第一個匹配的節(jié)點。
重繪與重排
瀏覽器下載完頁面中的所有組件——HTML標記挤茄、JavaScript如叼、CSS、圖片——之后會解析并生成兩個內部的數據結構:DOM樹(表示頁面結構)穷劈、渲染樹(表示DOM節(jié)點如何顯示)笼恰。當DOM的變化影響了元素的幾何屬性,瀏覽器會使渲染樹中受到影響的部分失效歇终,并重構社证,這個過程成為重排,完成后评凝,會重新繪制受影響的部分到屏幕追葡,該過程叫重繪。并不是所有的DOM變化都會影響幾何屬性奕短,這時只發(fā)生重繪宜肉。重繪和重排會導致web應用程序的UI反應遲鈍,應該盡量避免翎碑。
重排何時發(fā)生
當頁面布局的幾何屬性改變時就需要重排:
- 添加或刪除可見的DOM元素
- 元素位置改變
- 元素尺寸改變(包括:外邊據崖飘、內邊距、邊框厚度杈女、寬度朱浴、高度等屬性改變)
- 內容改變,例如:文本改變或圖片被另一個不同尺寸的圖片代替
- 頁面渲染器初始化
- 瀏覽器窗口尺寸改變
渲染樹變化的排隊與刷新
由于每次重排都會產生計算消耗达椰,大多數瀏覽器通過隊列化修改并批量執(zhí)行來優(yōu)化重排過程翰蠢。但是有些操作會導致強制刷新隊列并要求任務立刻執(zhí)行:
1. offsetTop,offsetLeft啰劲,offsetWidth梁沧,offsetHeight
2. scrollTop,scrollLeft蝇裤,scrollWidth廷支,scrollHeight
3. clientTop频鉴,clientLeft,clientWidth恋拍,clientHeight
4. getComputedStyle()
以上屬性和方法需要返回最新的布局信息垛孔,因此瀏覽器不得不執(zhí)行渲染隊列中的修改變化并觸發(fā)重排以返回正確的值。
最佳實踐:盡量將修改語句放在一起施敢,查詢語句放在一起周荐。
最小化重繪和重排
為了減少發(fā)生次數,應該合并多次DOM的樣式的修改僵娃,然后一次處理掉概作。
批量修改DOM
當你需要對DOM元素進行一系列操作時,可以通過以下步驟來減少重繪和重排的次數:
- 使元素脫離文檔
- 對其應用多重改變
- 把元素帶回文檔流
該過程會觸發(fā)兩次重排——第一步和第三步默怨,如果忽略這兩步讯榕,在第二步所產生的任何修改都會觸發(fā)一次重排。
有三種基本的方法可以使DOM脫離文檔:
- 隱藏元素匙睹,應用修改愚屁,重新顯示
- 使用文檔片段,在當前DOM之外構建一個子樹垃僚,再把它拷貝回文檔
- 將原始元素拷貝到一個脫離文檔的節(jié)點中集绰,修改副本,完成后再替換原始元素
推薦使用文檔片段谆棺,因為它們所產生的DOM遍歷和重排次數最少栽燕。
緩存緩存布局信息
當你查詢布局信息時,瀏覽器為了返回最新值改淑,會刷新隊列并應用所有變更碍岔。
最佳實踐:盡量減少布局信息的獲取次數,獲取后把它賦值給局部變量朵夏,然后操作局部變量蔼啦。
讓元素脫離動畫流
用展開、折疊的方式來顯示和隱藏部分頁面是一種常見的交互模式仰猖。通常包括展開區(qū)域的幾何動畫捏肢,并將頁面其他部分推向下方。一般來說饥侵,重排只影響渲染樹中的一小部分鸵赫,但也可能影響很大的部分,甚至整個渲染樹躏升。瀏覽器所需要重排的次數越少辩棒,應用程序的響應速度就越快。當一個動畫改變整個頁面的余下部分時,會導致大規(guī)模重排一睁。節(jié)點越多情況越差钻弄。避免大規(guī)模的重排:
1. 使用絕對定位頁面上的動畫元素,將其脫離文檔流者吁。
2. 應用動畫
3. 當動畫結束時回恢復定位窘俺,從而只會下移一次文檔的其他元素。
這樣只造成了頁面的一個小區(qū)域的重繪砚偶,不會產生重排并重繪頁面的大部分內容批销。
:hover
如果有大量元素使用了:hover洒闸,那么會降低響應速度染坯。此問題在IE8中更為明顯。
事件委托
當頁面中存在大量元素丘逸,并且每一個都要一次或多次綁定事件處理器時单鹿,這種情況可能會影響性能,每綁定一個事件處理器都是有代價的深纲,它要么加重了頁面負擔(更多的代碼仲锄、標簽),要么增加了運行期的執(zhí)行時間湃鹊。需要訪問和修改的DOM元素越多儒喊,應用程序就越慢,特別是事件綁定通常發(fā)生在onload時币呵,此時對每一個富交互應用的網頁來說都是一個擁堵的時刻怀愧。事件綁定占用了處理事件,而且瀏覽器要跟蹤每個事件處理器余赢,這也會占用更多的內存芯义。這些事件處理器中的絕大部分都可能不會被觸發(fā)。
事件委托原理:事件逐層冒泡并能被父級元素捕獲妻柒。使用事件代理扛拨,只需要給外層元素綁定一個處理器,就可以處理在其子元素上觸發(fā)的所有事件举塔。
根據DOM標準绑警,每個事件都要經歷三個階段:
1. 捕獲
2. 到達目標
3. 冒泡
IE不支持捕獲,但是對于委托而言央渣,冒泡已經足夠计盒。
<body>
<div>
<ul id="menu">
<li>
<a href="menu1.html">menu #1</a>
</li>
<li>
<a href="menu1.html">menu #2</a>
</li>
</ul>
</div>
</body>
在以上的代碼中,當用戶點擊鏈接“menu #1”痹屹,點擊事件首先從a標簽元素收到章郁,然后向DOM樹上層冒泡,被li標簽接收然后是ul標簽然后是div標簽,一直到達document的頂層甚至window暖庄。
委托實例:阻止默認行為(打開鏈接)聊替,只需要給所有鏈接的外層UL"menu"元素添加一個點擊監(jiān)聽器,它會捕獲并分析點擊是否來自鏈接培廓。
document.getElementById('menu').onclick = function(e) {
//瀏覽器target
e=e||window.event惹悄;
var target = e.target||e.srcElement;
var pageid肩钠,hrefparts泣港;
//只關心hrefs,非鏈接點擊則退出价匠,注意此處是大寫
if (target.nodeName !== 'A') {
return;
}
//從鏈接中找出頁面ID
hrefparts = target.href.split('/')当纱;
pageid = hrefparts[hrefparts.length-1];
pageid = pageid.replace('.html','');
//更新頁面
ajaxRequest('xhr.php?page='+id,updatePageContents);
//瀏覽器阻止默認行為并取消冒泡
if (type of e.preventDefault === 'function') {
e.preventDefault();
e.stopPropagation();
} else {
e.returnValue=false;
e.cancelBubble=true;
}
};
跨瀏覽器兼容部分:
1. 訪問事件對象,并判斷事件源
2. 取消文檔樹中的冒泡(可選)
3. 阻止默認動作(可選)
算法和流程控制
循環(huán)
循環(huán)的類型
ECMA-262標準第三版定義了javascript的基本語法和行為踩窖,其中共有四種循環(huán)坡氯。
-
第一種是標準的for循環(huán)。for循環(huán)是javascript最常用的循環(huán)結構洋腮,直觀的代碼封裝風格被開發(fā)者喜愛箫柳。它由四部分組成:初始化、前測條件啥供、后執(zhí)行體悯恍、循環(huán)體。
for (var i=0;i<10;i++){ //do something }
while循環(huán)伙狐。while循環(huán)是最簡單的前測循環(huán)涮毫,由一個前測條件和一個循環(huán)體構成。
do-while循環(huán)是javascript唯一一種后測循環(huán)鳞骤,由一個循環(huán)體和一個后測條件組成窒百,至少會執(zhí)行一次。
for-in循環(huán)豫尽「萆遥可以枚舉任何對象的屬性名。
循環(huán)的性能
JavaScript提供的四種循環(huán)類型中美旧,只有for-in
循環(huán)比其他幾種明顯要慢渤滞。因為每次迭代操作會同時搜索實例或原型屬性,for-in
循環(huán)的每次迭代都會產生更多開銷榴嗅。速度只有其他類型循環(huán)的七分之一妄呕。除非你明確需要迭代一個屬性數量未知的對象,否則應該避免使用for-in
循環(huán)嗽测。如果你需要遍歷一個數量有限的已知屬性列表绪励,使用其他循環(huán)類型會更快肿孵,比如數組。
除for-in
外疏魏,其他循環(huán)類型的性能都差不多停做,類型的選擇應該基于需求而不是性能。
提高循環(huán)的性能
- 減少每次迭代處理的事務
- 減少迭代的次數