《高性能JavaScript》--讀書筆記

第一章 加載和運行

延遲腳本 defer

該屬性代表腳本在執行期間不會影響到頁面的構造,腳本會先下載但被延遲到整個頁面都解析完畢後再運行.只適用於外部腳本javascript

<script src="js/test.js" defer></script>
<div>123</div>
<script>
    alert('script');
    window.onload = function(){
        alert('loaded');
    }
</script>
<!-- 輸出順序 123 script defer loaded   -->

異步腳本 async

只適用於外部腳本,告訴瀏覽器當即下載腳本,同時不影響頁面的解析,先下載完先執行,沒有必定的順序,但必定會在頁面load事件前執行php

總結

有幾種方法能夠減小JavaScript 對性能的影響:html

  • 將全部<script>標籤放置在頁面的底部,緊靠</body>的上方。此法能夠保證頁面在腳本
    運行以前完成解析。
  • 將腳本成組打包。頁面的<script>標籤越少,頁面的加載速度就越快,響應也更加迅速。不論外部腳本文件仍是內聯代碼都是如此。
  • <script>標籤添加defer屬性
  • 動態建立<script>元素,用它下載並執行代碼
  • 用XHR 對象下載代碼,並注入到頁面中
  • The YUI 3 approach
  • The LazyLoad library
  • The LABjs library

第二章 數據訪問

在JavaScript 中有四種基本的數據訪問位置:前端

  • 直接量 直接量僅僅表明本身,而不存儲於特定位置。JavaScript 的直接量包括:字符串,數字,布爾值,對象,數組,函數,正則表達式,具備特殊意義的空值,以及未定義。
  • 變量 開發人員使用var 關鍵字建立用於存儲數據值
  • 數組項 具備數字索引,存儲一個JavaScript 數組對象。
  • 對象成員 具備字符串索引,存儲一個JavaScript 對象。

總的來講,直接量和局部變量的訪問速度要快於數組項和對象成員的訪問速度。java

標識符解析過程

此過程搜索運行期上下文的做用域鏈,查找同名的標識符。搜索工做從運行函數的激活目標之做用域鏈的前端開始。若是找到了,那麼就使用這個具備指定標識符的變量;若是沒找到,搜索工做將進入做用域鏈的下一個對象。此過程持續運行,直到標識符被找到,或者沒有更多對象可用於搜索,這種狀況下標識符將被認爲是未定義的。web

標識符識別性能

在運行期上下文的做用域鏈中,一個標識符所處的位置越深,它的讀寫速度就越慢。因此,函數中局部變量的訪問速度老是最快的,而全局變量一般是最慢的(優化的JavaScript引擎在某些狀況下能夠改變這種情況)。請記住,全局變量老是處於運行期上下文做用域鏈的最後一個位置,因此老是最遠才能觸及的。
你能夠經過這種方法減輕重複的全局變量訪問對性能的影響:首先將全局變量的引用存儲在一個局部變量中,而後使用這個局部變量代替全局變量。ajax

改變做用域鏈

with 表達式
當代碼流執行到一個with 表達式時,運行期上下文的做用域鏈被臨時改變了。一個新的可變對象將被建立,它包含指定對象的全部屬性。此對象被插入到做用域鏈的前端,意味着如今函數的全部局部變量都被推入第二個做用域鏈對象中,因此訪問代價更高了。
try-catch 表達式的catch 子句
當try 塊發生錯誤時,程序流程自動轉入catch 塊,並將異常對象推入做用域鏈前端的一個可變對象中。在catch塊中,函數的全部局部變量如今被放在第二個做用域鏈對象中。正則表達式

閉包,做用域,內存

一般,一個函數的激活對象與運行期上下文一同銷燬。當涉及閉包時,激活對象就沒法銷燬了,由於引用仍然存在於閉包的[[Scope]]屬性中。這意味着腳本中的閉包與非閉包函數相比,須要更多內存開銷。算法

function assignEvents(){
    var id = "xdi9592";
    document.getElementById("save-btn").onclick = function(event){
        saveDocument(id);
    };
}

注意閉包中使用的兩個標識符,id 和saveDocument,存在於做用域鏈第一個對象以後的位置上。這是閉包最主要的性能關注點:你常常訪問一些範圍以外的標識符,每次訪問都致使一些性能損失。
關於域外變量的處理建議,減輕對運行速度的影響:將經常使用的域外變量存入局部變量中,而後直接訪問局部變量。編程

對象成員

每深刻原形鏈一層都會增長性能損失。記住,搜索實例成員的過程比訪問直接量或者局部變量負擔更重,因此增長遍歷原形鏈的開銷正好放大了這種效果。
因爲對象成員可能包含其它成員,例如不太常見的寫法window.location.href這種模式。每遇到一個點號,JavaScript 引擎就要在對象成員上執行一次解析過程。成員嵌套越深,訪問速度越慢。
通常來講,若是在同一個函數中你要屢次讀取同一個對象屬性,最好將它存入一個局部變量。以局部變量替代屬性,避免多餘的屬性查找帶來性能開銷。在處理嵌套對象成員時這點特別重要

總結

在JavaScript中,數據存儲位置能夠對代碼總體性能產生重要影響。有四種數據訪問類型:直接量,變量,數組項,對象成員。它們有不一樣的性能考慮。

  • 直接量和局部變量訪問速度很是快,數組項和對象成員須要更長時間。
  • 局部變量比域外變量快,由於它位於做用域鏈的第一個對象中。變量在做用域鏈中的位置越深,訪問所需的時間就越長。全局變量老是最慢的,由於它們老是位於做用域鏈的最後一環。
  • 避免使用with 表達式,由於它改變了運行期上下文的做用域鏈。並且應當當心對待try-catch 表達式的catch子句,由於它具備一樣效果。
  • 嵌套對象成員會形成重大性能影響,儘可能少用。
  • 一個屬性或方法在原形鏈中的位置越深,訪問它的速度就越慢。

通常來講,你能夠經過這種方法提升JavaScript 代碼的性能:將常用的對象成員,數組項,和域外變量存入局部變量中。而後,訪問局部變量的速度會快於那些原始變量。

第三章 DOM 編程

DOM 訪問和修改

訪問或修改元素最壞的狀況是使用循環執行此操做,特別是在HTML 集合中使用循環。

function innerHTMLLoop() {
    for (var count = 0; count < 15000; count++) {
        document.getElementById('here').innerHTML += 'a';
    }
}

這段代碼的問題是,在每次循環單元中都對DOM 元素訪問兩次:一次讀取innerHTML 屬性內容,另外一次寫入它。
一個更有效率的版本將使用局部變量存儲更新後的內容,在循環結束時一次性寫入:

function innerHTMLLoop2() {
    var content = '';
    for (var count = 0; count < 15000; count++) {
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;
}

innerHTML 與DOM 方法比較

它們的性能如何?答案是:性能差異不大,可是,在全部瀏覽器中,innerHTML 速度更快一些,除
了最新的基於WebKit 的瀏覽器(Chrome 和Safari)。

HTML 集合

HTML 集合是用於存放DOM 節點引用的類數組對象。下列函數的返回值就是一個集合:

  • document.getElementsByName()
  • document.getElementsByTagName()
  • document.getElementsByClassName()
  • document.images 頁面中全部的<img>元素
  • document.links 全部的<a>元素
  • document.forms 全部表單
  • document.forms[0].elements 頁面中第一個表單的全部字段

這些方法和屬性返回HTMLCollection 對象,是一種相似數組的列表。它們不是數組(由於它們沒有諸
如push()或slice()之類的方法),可是提供了一個length屬性,和數組同樣你可使用索引訪問列表中的元素。
HTML 集合實際上在查詢文檔,當你更新信息時,每次都要重複執行這種查詢操做。例如讀取集合中元
素的數目(也就是集合的length),這正是低效率的來源。
優化的辦法很簡單,只要將集合的length 屬性緩存到一個變量中,而後在循環判斷條件中使用這個變

function loopCacheLengthCollection() {
    var coll = document.getElementsByTagName('div'),
    len = coll.length;
    for (var count = 0; count < len; count++) {
        //more code
    }
}

通常來講,對於任何類型的DOM 訪問,若是同一個DOM 屬性或方法被訪問一次以上,最好使用一個
局部變量緩存此DOM成員。當遍歷一個集合時,第一個優化是將集合引用存儲於局部變量,並在循環以外緩存length 屬性。而後,若是在循環體中屢次訪問同一個集合元素,那麼使用局部變量緩存它。

DOM 漫談

querySelectorAll()
IE8+此函數不返回HTML集合,因此返回的節點不呈現文檔的「存在性結構」。這就避免了本章前面提到的HTML 集合所固有的性能問題(以及潛在的邏輯問題)。

查詢並刷新渲染樹改變

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (currentStyle in IE)

當你查詢佈局信息如偏移量、滾動條位置,或風格屬性時,瀏覽器刷新隊列並執行全部修改操做,以返回最新的數值。最好是儘可能減小對佈局信息的查詢次數,查詢時將它賦給局部變量,並用局部變量參與計算。

最小化重繪和重排版

重排版和重繪代價昂貴,因此,提升程序響應速度一個好策略是減小此類操做發生的機會。爲減小發生次數,你應該將多個DOM 和風格改變合併到一個批次中一次性執行。
當你須要對DOM 元素進行屢次修改時,你能夠經過如下步驟減小重繪和重排版的次數:

  • 從文檔流中摘除該元素
  • 對其應用多重改變
  • 將元素帶回文檔中

經常使用方法:

  1. 隱藏元素,進行修改,而後再顯示它。
    javascript var ul = document.getElementById('mylist'); ul.style.display = 'none'; appendDataToElement(ul, data); ul.style.display = 'block';
  2. 使用一個文檔片段在已存DOM 以外建立一個子樹,而後將它拷貝到文檔中。
    javascript var fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); document.getElementById('mylist').appendChild(fragment);
  3. 將原始元素拷貝到一個脫離文檔的節點中,修改副本,而後覆蓋原始元素。

推薦儘量使用文檔片段(第二種解決方案)由於它涉及最少數量的DOM 操做和重排版

將元素提出動畫流

使用如下步驟能夠避免對大部分頁面進行重排版:

  1. 使用絕對座標定位頁面動畫的元素,使它位於頁面佈局流以外。
  2. 啓動元素動畫。當它擴大時,它臨時覆蓋部分頁面。這是一個重繪過程,但隻影響頁面的一小部分,避免重排版並重繪一大塊頁面。
  3. 當動畫結束時,從新定位。

事件託管

當頁面中存在大量元素,並且每一個元素有一個或多個事件句柄與之掛接(例如onclick)時,可能會影
響性能
一個簡單而優雅的處理DOM 事件的技術是事件託管。它基於這樣一個事實:事件逐層冒泡總能被父元
素捕獲。採用事件託管技術以後,你只須要在一個包裝元素上掛接一個句柄,用於處理子元素髮生的全部事件。

總結

爲減小DOM 編程中的性能損失,請牢記如下幾點:

  • 最小化DOM 訪問,在JavaScript 端作儘量多的事情。
  • 在反覆訪問的地方使用局部變量存放DOM 引用.
  • 當心地處理HTML 集合,由於他們表現出「存在性」,老是對底層文檔從新查詢。將集合的length 屬性緩存到一個變量中,在迭代中使用這個變量。若是常常操做這個集合,能夠將集合拷貝到數組中。
  • 若是可能的話,使用速度更快的API,諸如querySelectorAll()和firstElementChild。
  • 注意重繪和重排版;批量修改風格,離線操做DOM 樹,緩存並減小對佈局信息的訪問。
  • 動畫中使用絕對座標,使用拖放代理。
  • 使用事件託管技術最小化事件句柄數量。

第四章 算法和流程控制

循環性能

在JavaScript 提供的四種循環類型中,只有一種循環比其餘循環明顯要慢:for-in 循環。
因爲每次迭代操做要搜索實例或原形的屬性,for-in 循環每次迭代都要付出更多開銷。所以推薦的作法以下:
除非你須要對數目不詳的對象屬性進行操做,不然避免使用for-in 循環其餘循環類型性能至關,難以肯定哪一種循環更快。選擇循環類型應基於需求而不是性能。

基於函數的迭代

items.forEach(function(value, index, array){
    process(value);
});

儘管基於函數的迭代顯得更加便利,它仍是比基於循環的迭代要慢一些。

條件表達式

if-else 與switch 比較

大多數狀況下switch 表達式比if-else 更快,但只有當條件體數量很大時才明顯更快。二者間
的主要性能區別在於:當條件體增長時,if-else 性能負擔增長的程度比switch更多。所以,咱們的天然傾向認爲條件體較少時應使用if-else 而條件體較多時應使用switch 表達式,若是從性能方面考慮也是正確的。

優化if-else

優化if-else的目標老是最小化找到正確分支以前所判斷條件體的數量。最簡單的優化方法是將最多見的條件體放在首位

總結

  • for,while,do-while 循環的性能特性類似,誰也不比誰更快或更慢。
  • 除非你要迭代遍歷一個屬性未知的對象,不然不要使用for-in 循環。
  • 通常來講,switch 老是比if-else 更快,但並不老是最好的解決方法。
  • 當判斷條件較多時,查表法比if-else 或者switch 更快。
  • 若是你遇到一個棧溢出錯誤,將方法修改成一個迭代算法或者使用製表法能夠避免重複工做。

第五章 字符串和正則表達式

字符串鏈接

加和加等於操做
str += "one" + "two";
str = str + "one" + "two";

這就避免了使用臨時字符串,由於賦值表達式開頭以str爲基礎,一次追加一個字符串,從左至右依次
鏈接。

Array.prototype.join

str = ["a","b","c"].join("");

String.prototype.concat

str = str.concat(s1, s2, s3);

正則表達式優化

第六章 響應接口

瀏覽器UI 線程

瀏覽器限制

這是一個有必要的限制,確保惡意代碼編寫者不能經過無盡的密集操做鎖定用戶瀏覽器或計算機。此類限制有兩個:調用棧尺寸限制和長時間腳本限制。

用定時器讓出時間片

function processArray(items, process, callback){
    var todo = items.concat(); //create a clone of the original
    setTimeout(function(){
        process(todo.shift());
        //if there's more items to process, create another timer
        if(todo.length > 0){
            setTimeout(arguments.callee, 25);
        } else {
            callback(items);
        }
    }, 25);
}

setTimeout第二個參數指出何時應當將任務添加到UI 隊列之中,並非說那時代碼將被執行。

分解任務

若是函數運行時間太長,它能夠拆分紅一系列更小的步驟,把獨立方法放在定時器中調用。你能夠將每一個函數都放入一個數組,而後使用前一節中提到的數組處理模式:

function saveDocument(id){
    var tasks = [openDocument, writeText, closeDocument, updateUI];
    setTimeout(function(){
        //execute the next task
        var task = tasks.shift();
        task(id);
        //determine if there's more
        if (tasks.length > 0){
            setTimeout(arguments.callee, 25);
        }
    }, 25);
}
限時運行代碼

可經過原生的Date 對象跟蹤代碼的運行時間。這是大多數JavaScript 分析工具所採用的工做方式:

var start = +new Date(),
stop;
someLongProcess();
stop = +new Date();
if(stop-start < 50){
    alert("Just about right.");
} else {
    alert("Taking too long.");
}

Web Workers

工人線程的運行環境由下列部分組成:

  • 一個瀏覽器對象,只包含四個屬性:appName, appVersion, userAgent, 和platform
  • 一個location 對象(和window 裏的同樣,只是全部屬性都是隻讀的)
  • 一個self 對象指向全局工人線程對象
  • 一個importScripts()方法,使工人線程能夠加載外部JavaScript 文件
  • 全部ECMAScript 對象,諸如Object,Array,Data,等等。
  • XMLHttpRequest 構造器
  • setTimeout()和setInterval()方法
  • close()方法可當即中止工人線程

要建立網頁工人線程,你必須傳入這個JavaScript 文件的URL:

var worker = new Worker("code.js");

工人線程和網頁代碼經過事件接口進行交互。網頁代碼可經過postMessage()方法向工人線程傳遞數據,它接收單個參數,即傳遞給工人線程的數據。此外,在工人線程中還有onmessage事件句柄用於接收信息。
當工人線程經過importScripts()方法加載外部JavaScript 文件,它接收一個或多個URL參數,指出要加載的JavaScript文件網址。工人線程以阻塞方式調用importScripts(),直到全部文件加載完成並執行以後,腳本才繼續運行。因爲工人線程在UI線程以外運行,這種阻塞不會影響UI響應。

實際用途

網頁工人線程適合於那些純數據的,或者與瀏覽器UI 不要緊的長運行腳本。它看起來用處不大,而網
頁應用程序中一般有一些數據處理功能將受益於工人線程,而不是定時器。
任何超過100毫秒的處理,都應當考慮工人線程方案是否是比基於定時器的方案更合適。固然,還要基
於瀏覽器是否支持工人線程。

總結

有效地管理UI 線程就是要確保JavaScript 不能運行太長時間,以避免影響用戶體驗。最後,請牢記以下幾點:

  • JavaScript運行時間不該該超過100毫秒。過長的運行時間致使UI更新出現可察覺的延遲,從而對總體用戶體驗產生負面影響。
  • 定時器可用於安排代碼推遲執行,它使得你能夠將長運行腳本分解成一系列較小的任務。
  • 網頁工人線程是新式瀏覽器才支持的特性,它容許你在UI線程以外運行JavaScript代碼而避免鎖定UI。

第七章 Ajax

在現代高性能JavaScript 中使用的三種技術是XHR,動態腳本標籤插入和多部分的XHR。

XMLHttpRequest

var url = '/data.php';
var params = ['id=934875','limit=20'];
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
    if (req.readyState === 4) {
        var responseHeaders = req.getAllResponseHeaders(); // Get the response headers.
        var data = req.responseText; // Get the data.
        // Process the data here...
    }
}
req.open('GET', url + '?' + params.join('&'), true);
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // Set a request header.
req.send(null); // Send the request.

動態腳本標籤插入

var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName('head')[0].appendChild(scriptElement);

多部分XHR

這裏介紹最新的技術,多部分XHR(MXHR)容許你只用一個HTTP 請求就能夠從服務器端獲取多個
資源。它經過將資源(能夠是CSS 文件,HTML 片斷,JavaScript代碼,或base64編碼的圖片)打包成一個由特定分隔符界定的大字符串,從服務器端發送到客戶端。
使用此技術有一些缺點,其中最大的缺點是以此方法得到的資源不能被瀏覽器緩存

第八章 編程實踐

避免二次評估

大多數狀況下,不必使用eval_r()或Function(),若是可能的話,儘可能避免使用它們。至於另外兩個函數,setTimeout()和setInterval(),建議第一個參數傳入一個函數而不是一個字符串。

使用對象/數組直接量

位操做運算符

位與& 兩個操做數的位都是1,結果纔是1
位或| 有一個操做數的位是1,結果就是1
位異^或 兩個位中只有一個1,結果纔是1
位非~ 遇0返回1,反之亦然

for (var i=0, len=rows.length; i < len; i++){
    if (i & 1) {  //判斷奇偶數
        className = "odd";
    } else {
        className = "even";
    }
}

總結

  • 經過避免使用eval_r()和Function()構造器避免二次評估。此外,給setTimeout()和setInterval()傳遞函數參數而不是字符串參數。
  • 建立新對象和數組時使用對象直接量和數組直接量。它們比非直接量形式建立和初始化更快。
  • 避免重複進行相同工做。當須要檢測瀏覽器時,使用延遲加載或條件預加載。
  • 當執行數學遠算時,考慮使用位操做,它直接在數字底層進行操做。
  • 原生方法老是比JavaScript 寫的東西要快。儘可能使用原生方法。

第九章 構建和部署高性能JavaScript 應用

第十章 工具

相關文章
相關標籤/搜索