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

《高性能JavaScript》

本書小結匯總

  • 第1章 加載和執行
    • </body>閉合標籤之前,將所有的<script>標籤放到頁面底部。這能確保在腳本執行前頁面已經完成了渲染。
    • 合併腳本。頁面中的<script>標籤越少,加載也就越快,響應也更迅速。無論外鏈文件還是內嵌腳本都是如此。
    • 有多種無阻塞下載js的方法:
      • 使用<script>標籤的defer屬性;
      • 使用動態創建的<script>元素來下載並執行代碼;
      • 使用XHR對象下載js代碼並注入頁面中。
  • 第2章 數據存取
    • 訪問字面量和局部變量的速度最快,相反,訪問數組元素和對象成員相對較慢。
    • 由於局部變量存在於作用域鏈的起始位置,因此訪問局部變量比訪問跨作用域變量更快。變量再作用域鏈中的位置越深,訪問所需時間就越長。由於全局變量總處在作用域鏈的最末端,因此訪問速度也是最慢的。
    • 避免使用with語句,因爲它會改變執行環境作用域鏈。同樣try-catch語句中的catch子句也有同樣的影響,要小心使用。
    • 嵌套的對象成員會明顯影響性能,儘量少用。
    • 屬性或方法在原型鏈中的位置越深,訪問它的速度也越慢。
    • 可以通過把常用的對象成員、數組元素、跨域變量保存在局部變量中來改善js性能,因爲局部變量訪問速度更快。
  • 第3章 DOM編程
    • 最小化DOM訪問次數,儘可能在js端處理。
    • 如果需要多次訪問某個DOM節點,請使用局部變量存儲它的引用。
    • 小心處理HTML集合,因爲它實時連繫着底層文檔。把集合的長度緩存到一個變量中,並在迭代中使用它。如果需要經常操作集合,建議把它拷貝到一個數組中。
    • 如果可以,使用速度更快的API,如querySelectorAll()和firstElementChild。
    • 要留意重繪和重排;批量修改樣式時,“離線”操作DOM樹,使用緩存,並減少訪問佈局信息的次數。
    • 動畫中使用絕對定位,使用拖放代理。
    • 使用事件委託來減少事件處理器的數量。
  • 第4章 算法和流程控制
    • for\while\do-while循環性能特性相當。
    • 避免使用for-in循環,除非要遍歷一個屬性數量未知的對象。
    • 改善循環性能的最佳方式是減少每次迭代的運算量和減少循環迭代次數。
    • 通常switch比 if-else 快。
    • 在判斷條件較多時,使用查找表比 if-else 和switch更快。
    • 瀏覽器的調用棧大小限制了遞歸的應用;棧溢出會導致其他代碼中斷運行。
    • 如果遇到棧溢出,可將遞歸改爲迭代算法,或使用Memoization方法避免重複計算。
  • 第5章 字符串和正則表達式
    • 當連接數量巨大或尺寸巨大的字符串時,數組項合併是唯一在IE7及以下版本中性能合理的方法;如果不需要考慮IE7及以下版本,數組項合併是最慢的字符串連接方法之一,推薦使用簡單的+和+=操作符代替,避免不必要的中間字符串。
    • 回溯既是正則匹配功能的基本組成部分,也是正則的低效之源。
    • 回溯失控發生在正則本應快速匹配的地方,但因爲某些特殊字符串匹配動作導致運行緩慢甚至瀏覽器崩潰。避免這個問題的辦法是:使相鄰字元互斥,避免嵌套量詞對同一字符串的相同部分多次匹配,通過重複利用預查的原子組去除不必要的回溯。
    • 提高正則效率的各種技術手段會有助於正則更快地匹配,並在非匹配位置上花更少的時間。
    • 正則並不總是完成工作的最佳工具,尤其當你只搜索字面字符串的時候。
    • 使用2個簡單的正則(一個去頭一個去尾)來處理大量字符串的去首尾空白能提供一個簡潔而跨瀏覽器的方法。
  • 第6章 快速響應的用戶界面
    • 任何js任務都不應當執行超過100毫秒。過長的運行時間會導致UI更新出現明顯的延遲。
    • js運行期間,瀏覽器響應用戶交互的行爲存在差異。但長時間運行會導致用戶體驗變得混亂、脫節。
    • 定時器可用來安排代碼延遲執行,可以把長時間運行腳本分解成一系列小任務。
    • Web Workers允許在UI線程外部執行js代碼,從而避免長時間運行腳本鎖定UI。
  • 第7章 Ajax
    • 選擇合適的數據傳輸方式和數據格式。
    • 減少請求樹,可通過合併JS和CSS文件,或使用MXHR。
    • 縮短頁面的加載時間,頁面主要內容加載完成後,用AJAX獲取那些次要的文件。
    • 確保你的代碼錯誤不會輸出給用戶,並在服務端處理錯誤。
    • 知道何時使用成熟的Ajax類庫,以及何時編寫自己的底層Ajax代碼。
  • 第8章 編程實踐
    • 通過避免使用eval()和Function()構造器來避免雙重求值帶來的性能消耗。同樣的,給setTimeout()和setInterval()傳遞函數而不是字符串作爲參數。
    • 儘量使用直接量創建對象和數組。直接量的創建和初始化都比非直接量形式要快。
    • 避免做重複的工作。當需要檢測瀏覽器時,可使用延遲加載或條件預加載。
    • 在進行數學計算時,考慮使用直接操作數字的二進制形式的位運算。
    • 儘量使用原生方法。
  • 第9章 構建並部署高性能js應用
    • 合併js文件以減少http請求數。
    • 使用YUI Compressor壓縮js文件。
    • 在服務端壓縮js文件(Gzip編碼)。
    • 通過正確設置HTTP響應頭來緩存js文件,通過向文件名增加時間戳來避免緩存問題。
    • 使用CDN提供js文件;CDN不僅可以提升性能,也爲你故那裏文件的壓縮與緩存。
  • 第10章 工具
    • 使用網絡分析工具找出加載腳本和頁面中其他資源的瓶頸,這會幫助你決定哪些腳本需要延遲加載,或者需要進一步分析。
    • 儘管傳統的經驗告訴我們要儘量減少http請求數,但把腳本儘可能延遲加載可以加快頁面渲染速度。
    • 使用性能分析工具找出腳本運行過程中速度慢的地方,檢查每個函數所消耗的時間,以及函數被調用的次數,通過調用棧自身提供的一些線索來找出需要集中精力優化的地方。
    • 儘管耗費的時間和調用次數通常是數據中最有價值的部分,但仔細觀察函數的調用過程,你也許會發現其他優化目標。

第1章 加載和執行

1.推薦將所有<script>標籤儘可能放到<body>標籤的底部,以儘量減少對整個頁面下載的影響。

2.合併腳本:減少頁面包含的<script>標籤數量,包括外鏈腳本和內嵌腳本。原因是http請求會帶來額外的性能開銷。

  • 如何減少script標籤數量?可以把多個文件合併成一個,如何合併:
    • 通過離線的打包工具
    • 雅虎的實時在線服務

3.無阻塞的腳本:儘管下載單個較大的js文件只產生一次http請求,卻會鎖死瀏覽器一大段時間。爲避免這種情況,需要向頁面中逐步加載js文件,這樣在某種程度上不會阻塞瀏覽器。無阻塞腳本的祕訣在於,在頁面加載完成後才加載js代碼,即在window對象的load事件出發後再下載腳本。

  • 如何實現無阻塞

    • 延遲的腳本:script標籤有一個defer屬性,此屬性指明本元素所含的腳本不會修改dom,因此代碼能安全地延遲執行。例如:<script src="file.js" defer/>。注意,帶有defer屬性的script元素是在window.onload執行之前被調用。

    • 動態腳本元素:用js動態創建script元素。重點在於,無論何時啓動下載,文件的下載和執行過程不會阻塞頁面其他進程。動態創建時放到head標籤比body標籤裏更保險。可以監聽script加載完成事件進行下一步操作,此處需要考慮瀏覽器兼容性。

    • XHR腳本注入:通過XHR對象下載js文件,最後通過創建動態script元素將代碼注入頁面中。這種方法的優點是,可以下載js代碼但不立即執行,可以推遲到需要的時候;瀏覽器兼容性好。缺點是js文件必須與所請求頁面處於相同的域,意味着不能從CDN下載。

    • 推薦做法:先添加動態加載所需代碼,然後加載初始化頁面所需的剩下的代碼。

      <script src="load.js"></script><!--存放loadScript方法定義-->
      <script>
          loadScript("the-rest.js",function(){//處理瀏覽器兼容性的方法,用於監聽腳本加載完成事件
             Application.init(); 
          });
      </script>
      
    • YUI3的方式:由頁面中的少量代碼來加載豐富的功能組件。

    • LazyLoad類庫:開源的無阻塞腳本加載工具。是loadScript函數的增強版,支持下載多個js文件。

    • LABjs:開源的無阻塞腳本加載工具。對加載過程更精細的控制,試圖同時下載儘可能多的代碼。

第2章 數據存取

1.數據存儲位置

1)字面量

字符串、數字、布爾值、對象、數組、函數、正則表達式,和特殊的null和undefined值。

2)本地變量

使用關鍵字var定義的數據存儲單元。

3)數組元素

存儲在js數組對象內部,以數字作爲索引。

4)對象成員

存儲在js對象內部,以字符串作爲索引。

2.管理作用域

1)作用域鏈和標識符解析

2)標識符解析的性能

函數中讀寫局部變量總是最快的,而讀寫全局變量通常是最慢的。(採用優化過的js引擎的瀏覽器,沒有類似的性能損失)

一個好的做法是:如果某個跨作用域的值在函數中被引用一次以上,那麼就把它存儲到局部變量裏。

3)改變作用域鏈

一般來說,一個執行環境的作用域鏈是不會改變的,但是有2個語句可以在執行時臨時改變作用域鏈:with語句和try-catch的catch子句。with不推薦使用,catch使用得當是推薦的。

4)動態作用域

with、try-catch的catch子句、包含eval()的函數,都是動態作用域。

經過優化的瀏覽器js引擎,嘗試通過分析代碼來確定哪些變量可以在特定時候被訪問,這些引擎視圖避開傳統作用域鏈的查找,取代以標識符索引的方式進行快速查找。設計動態作用域時,這種優化方式就失效了。因此只在必要時才使用動態作用域。

5)閉包、作用域和內存

閉包是js最強大的特性之一,它允許函數訪問局部作用域之外的數據。但使用閉包可能會導致性能問題。

問題所在:閉包的屬性包含了與執行環境作用域鏈相同的對象的引用。通常,函數的活動對象會隨着執行環境一同銷燬。但引入閉包時,由於引用仍然存在於閉包的實行中,因此集火對象無法被銷燬。這意味着閉包需要更多的內存開銷。尤其在IE瀏覽器中需要關注,由於IE使用非原生js對象來實現DOM對象,因此閉包會導致內存泄漏。

下圖是一個閉包的作用域鏈,當調用saveDocument(id)時,需要兩次遍歷作用域鏈,第一次從上至下遍歷至最後找到saveDocument,第二次從上至下遍歷至第2個作用域的最後找到id。因此,若頻繁調用,每次都會消耗查找作用域鏈的時間。

在這裏插入圖片描述

因此,最好小心地使用閉包,它同時關係到內存和執行速度。不過可以將常用的跨作用域變量存儲在局部變量中,然後在閉包內直接訪問局部變量來減輕閉包對執行速度的影響。

3.對象成員

對象成員包括屬性和方法。

訪問對象成員的速度比訪問字面量或變量要慢,在某些瀏覽器中比訪問數組元素還要慢。需先理解js中對象的本質。

1)原型

js中的對象是基於原型的。它定義並實現了一個新創建的對象所必須包含的成員列表。原型對象爲所有對象實例所共享,因此這些實例也共享了原型對象的成員。

對象通過一個內部屬性__proto__綁定到它的原型。一旦你創建一個內置對象(如Object何Array)的實例,它們就會自動擁有一個Object實例作爲原型。

因此,對象有2種成員類型:實例成員和原型成員。實例成員直接存在於對象實例中,原型成員從對象原型繼承而來。

示例:

var book={
    title: "High Performance JavaScript",
    publisher: "Yahoo! Press"
};

在這裏插入圖片描述

book.toString()被調用時,會從對象實例開始搜索名爲“toString”的成員。一旦book沒有名爲toString的成員,那麼會繼續搜索其原型對象,直到找到並執行。

可以使用hasOwnProperty()來判斷對象是否包含特定的屬性。

2)原型鏈

可以定義並使用構造函數來創建另一種類型的原型。

示例:

function Book(title,publisher){
    this.title=title;
    this.publisher=publisher;
}
Book.prototype.sayTitle=function(){
    alert(this.title);
};
var book1=new Book("高性能JavaScript","Yahoo! Press");
alert(book1 instanceof Book);//true
book1.sayTitle();//"高性能JavaScript"
alert(book1.toString());//[object Object]

使用構造函數來創建一個新的實例。實例book1的原型__proto__Book.prototype,而Book.prototype的原型是Object。

對象在原型鏈中存在的位置越深,找到某個屬性就越慢。每深入一層原型鏈都會增加性能損失,搜索實例成員比從字面量或局部變量中讀取數據代價更高,再加上遍歷原型鏈帶來的開銷,這讓性能問題更爲嚴重。

3)嵌套成員

對象成員可能包含其他成員。例如window.location.href,每次遇到點操作符,嵌套成員會導致js引擎搜索所有對象成員。

對象成員嵌套得越深,讀取速度就會越慢。

執行location.href總是比window.location.href要快,window.location.href也比window.location.href.toString()要快。如果這些屬性不是對象的實例屬性,那麼成員解析還需要搜索原型鏈,這會花更多的時間。

【大部分瀏覽器中,通過點表示法(object.name)操作和通過括號表示法(object[‘name’])操作並沒有明顯的區別,只有在Safari中,點符號始終會更快。】

4)緩存對象成員值

只在必要時使用對象成員。在同一個函數中沒有必要多次讀取同一個對象成員。

反例:

function hasEitherClass(element,className1,classname2){
    return element.className==className1 || element.className==className2;
}

上述代碼中,element.className被讀取了2次。

可以將值保存在局部變量中來減少一次查找,因爲局部變量的讀取速度要快得多,特別是在處理,嵌套對象成員時,這樣做會明顯提升執行速度。

優化如下:

function hasEitherClass(element,className1,classname2){
    var currentClassName=element.className;
    return currentClassName==className1 || currentClassName==className2;
}

第3章 DOM編程

1.瀏覽器中的DOM

瀏覽器中通常會把DOM和JavaScript獨立實現。

在IE中,js的實現名爲JScript,位於jscript.dll文件中;DOM的實現則存在另一個庫中,名爲mshtml.dll(內部稱爲Trident)。這個分離允許其他技術和語言,比如VBScript能共享使用DOM以及Trident提供的渲染函數。

Safari中的DOM和渲染是使用Webkit中的WebCore實現,js部分是由獨立的js引擎來實現。

Google Chrome同樣使用Webkit中的WebCore庫來渲染頁面,但js引擎是自己研發的,名爲V8。

Firefox的js引擎名爲SpiderMonkey,與名爲Gecko的渲染引擎相互獨立。

2.DOM訪問與修改

訪問DOM元素是有代價的。修改元素則代價更高,因爲它會導致瀏覽器重新計算頁面的幾何變化。最壞的情況是在循環中訪問或修改元素。因此,通用的經驗法則是:減少訪問DOM的次數,把運算儘量留在js這一端處理。

1)innerHTML對比DOM方法

修改頁面區域的2種方案:innerHTML屬性,document.createElement()

在除開最新版的Webkit內核之外的所有瀏覽器中,innerHTML會更快一些。最新版的Webkit內核瀏覽器使用document.createElement()更快一些。

使用數組合並字符串會讓innerHTML效率更高。

2)節點克隆

使用DOM方法更新頁面內容的另一途徑是克隆已有元素,而不是創建新元素。即使用element.cloneNode()替代document.createElement()。

3)HTML集合

HTML集合是包含了DOM節點引用的類數組對象。不是真正的數組。沒有push或slice等方法,但提供了length屬性,並且能以數字索引的方式訪問列表中的元素。

以下方法的返回值就是一個HTML集合:

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()

以下屬性返回HTML集合:

  • document.images:頁面中所有img元素
  • document.links:所有a元素
  • document.forms:所有表單元素
  • document.form[0].elements:頁面中第一個表單的所有字段

事實上,HTML集合一直與文檔保持着連接,每次你需要最新的信息時,都會重複執行查詢的過程,哪怕只是獲取即合理的元素個數。這正是低效之源。

循環遍歷HTML集合時,可以通過緩存集合的length或拷貝一個HTML集合到普通數組中,再進行循環,執行速度會更快。循環過程中根據索引取集合元素中某一項時,設置局部變量執行速度更快。

3.遍歷DOM

1)獲取DOM元素

使用childNodes或nextSibling

老版本IE中,nextSibling性能更好。

2)元素節點

大部分現代瀏覽器提供的API只返回元素節點,推薦使用這些API代替js原生的,因爲這些實現過濾的效率要高。

屬性名 被替代屬性 是否支持IE8
children childNodes
childElementCount childNodes.length
firstElementChild firstChild
lastElementChild lastChild
nextElementSibling nextSibling
previousElementSibling previousSibling

3)選擇器API

除了getElementById和getElementsByTagName,使用CSS選擇器也是一種定位節點的便利途徑。現代瀏覽器也提供了一個querySelectorAll的原生DOM方法,這種方式比使用js和dom來遍歷查找元素要快得多。

querySelectorAll返回一個NodeList,不是HTML集合,因此不會對應實時的文檔結構,避免了HTML集合引起的性能問題。

處理大量組合查詢,使用querySelectorAll效率更高。querySelectorAll支持IE8。

querySelector方法返回第一個匹配的節點,同樣效率更高。

4.重繪與重排

瀏覽器下載完頁面中的所有內容(HTML、JS、CSS、圖片)後會解析並生成2個內部數據結構:DOM樹(頁面結構)和渲染樹(DOM節點如何顯示)。

DOM樹中的每一個需要顯示的節點在渲染樹中至少存在一個對應的節點(隱藏的DOM元素再渲染樹中沒有對應的節點)。一旦DOM和渲染樹構建完成,瀏覽器就開始顯示(繪製)頁面元素。

渲染樹中的節點被稱爲“幀”或“盒”。

重排:當DOM的變化影響了元素的幾何屬性(寬和高),瀏覽器需要重新計算元素的幾何屬性,同時其他元素的幾何屬性和位置也會因此受到影響。瀏覽器會使渲染樹中受到影響的部分失效,並重新構造渲染樹。

重繪:完成重排後,瀏覽器會重新繪製受影響的部分到屏幕中。

發生重繪不一定發生重排:例如,只改變一個元素的背景色不會影響元素寬高,這時只發生一次重繪,不需要重排,因爲元素佈局沒有改變。

影響性能:重繪和重排都是代價昂貴的操作,它們會導致WEB應用程序的UI反應遲鈍,所以應當儘可能減少這類過程的發生。

1)重排何時發生

當頁面佈局和幾何屬性改變時就需要重排:

  • 添加或刪除可見DOM元素
  • 元素位置改變
  • 元素尺寸改變(margin/padding/border-width/width/height等)
  • 內容改變(例如:文本改變或圖片被另一個不同尺寸的圖片替代)
  • 頁面渲染器初始化
  • 瀏覽器窗口尺寸改變

有些改變會觸發整個頁面的重排,例如,當滾動條出現時。

2)渲染樹變化的排隊與刷新

以下獲取佈局信息的操作會強制刷新隊列並要求計劃任務立刻執行:

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

以上 屬性和方法需要返回最新的佈局信息,因此瀏覽器不得不執行渲染隊列中的”待處理變化“並觸發重排以返回正確的值。

如果需要多次調用以上屬性,儘量避免與會導致重繪、重排的操作穿插調用,放在一起連續調用執行速度更快。

3)最小化重繪和重排

提高程序響應速度的一個策略就是減少重繪和重排操作的發生。爲減少發生次數,應該合併多次對DOM和樣式的修改,然後依次處理掉。

改變樣式

示例:

var el=document.getElementById('mydiv');
el.style.borderLeft='1px';
el.style.borderRight='2px';
el.style.padding='5px';

以上示例中有3個樣式屬性被改變,每一個都會影響元素的幾何結構。最糟糕的情況下會導致瀏覽器觸發3次重排。

大部分現代瀏覽器爲此做了優化,只會觸發一次重排,但在舊版瀏覽器中或者使用計時器時,仍然效率低下。

優化後代碼:

//合併改變樣式的操作
var el=document.getElementById('mydiv');
el.style.cssText+='border-left:1px;border-right:2px;padding:5px;';

或者也可以直接修改class,而不是修改內聯樣式。直接改變class的方法更清晰,更易於維護;有助於保持樣式與結構分離,但是會帶來輕微的性能影響——改變類時需要檢查級聯樣式。

優化代碼如下:

var el=document.getElementById('mydiv');
el.className='active';
批量修改DOM

當需要對DOM元素進行一系列操作時,可通過以下步驟減少重繪和重排的次數:

  1. 使元素脫離文檔流
  2. 對其應用多重改變
  3. 把元素帶回文檔中

該過程裏會觸發兩次重排——第1步和第3步,如果忽略這兩個步驟,那麼在第2步所產生的任何修改都會觸發一次重排。

有3種基本方法可以使DOM脫離文檔:

  • 隱藏元素,應用修改,重新顯示
  • 使用文檔片段(document fragment)在當前DOM之外構建一個子樹,再把它拷貝迴文檔
  • 將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成後再替換原始元素

推薦第2種方法,因爲使用文檔片段所產生的DOM遍歷和重排次數最少。

4)緩存佈局信息

瀏覽器嘗試通過隊列化修改和批量執行的方式最小化重排次數。當你查詢佈局信息時,比如獲取偏移量、滾動位置或計算出的樣式值時,瀏覽器爲了返回最新值,會刷新隊列並應用所有變更。最好的做法是儘量減少佈局信息的獲取次數,獲取後把它賦值給局部變量,然後再操作局部變量。

5)讓元素脫離動畫流

當頁面頂部的一個動畫推移頁面整個餘下的部分時,會導致一次代價昂貴的大規模重排,讓用戶感到頁面一頓一頓的。渲染樹中需要重新計算的節點越多,情況就越糟。

使用以下步驟可避免頁面中的大部分重排:

  • 使用絕對位置定位頁面上的動畫元素,將其脫離文檔流。
  • 讓元素動起來。當它擴大時,會臨時覆蓋部分頁面。但這只是頁面一個小區域的重繪過程,不會產生重排並重繪頁面的大部分內容。
  • 當動畫結束時恢復定位,從而只會下移一次文檔的其他元素。

6)IE和:hover

從IE7開始,IE允許使用:hover這個css僞選擇器。但是,如果有大量元素使用了:hover,那麼會降低響應速度。這個問題在IE8中更爲明顯。

5.事件委託

綁定事件處理器性能缺點:

  • 每綁定一個事件處理器都是有代價的,要麼是加重了頁面負擔(更多的標籤和js代碼),要麼是增加了運行期的執行時間。

  • 需要訪問和修改的dom元素越多,應用程序也就越慢,特別是事件綁定通常發生在onload時。事件綁定佔用了處理時間,瀏覽器需要跟蹤每個事件處理器,這會佔用更多的內存。

  • 這些事件處理器中的絕大部分都不再需要,因此有很多工作是沒必要的。(不是100%的按鈕或鏈接會被用戶點擊)

優化方法:事件委託。

事件委託基於一個事實:事件逐層冒泡並能被父級元素捕獲。使用事件委託,只需給外層元素綁定一個處理器,就可以處理在其子元素上觸發的所有事件。

示例代碼:

document.getElementById('menu').onclick=function(e){
    //瀏覽器target
    e=e||window.event;
    var target=e.target||e.srcElement;
    
    var pageid,hrefparts;
    
    //只關心hrefs,非鏈接點擊則退出
    if(targets.nodeName!=='A'){
        return;
    }
    //從鏈接中找出頁面id
    hrefparts=target.href.split('/');
    pageid=hrefparts[hrefparts.length-1];
    pageid=pageid.replace('.html','');
    //更新頁面
    ajaxRequest('xhr.php?page='+id,updatePageContents);
    //瀏覽器阻止默認行爲並取消冒泡
    if(typeof e.preventDefault === 'function'){
        e.preventDefault();
        e.stopPropagation();
    }else{
        e.returnValue=false;
        e.cancelBubble=true;
    }
}

第4章 算法和流程控制

代碼組織結構和解決具體問題的思路是影響代碼性能的主要因素。

1.循環

1)循環的4種類型

  • 標準for循環:

    • 由4部分組成:初始化、前測條件、後執行體、循環體。
  • while循環:

    • 由2部分組成:前測條件、循環體。
  • do-while循環:

    • 唯一一種後測循環,由2部分組成:循環體、後測條件。
  • for-in循環:

    • 可以枚舉任何對象的屬性名。所遍歷的屬性包括對象實例屬性以及從原型鏈中繼承而來的屬性。

2)循環性能

for-in

for-in比其他三種明顯慢。因爲每次遍歷都會同時搜索實例或原型屬性,會產生更多開銷。對比其他方式,for-in最終只有其他方式速度的1/7。所以儘量避免使用for-in循環。

可以通過取出對象的屬性列表,遍歷屬性列表,減少for-in的開銷。優化代碼如下:

var props=["prop1","prop2"],i=0;
while(i<props.length){
    process(object[props[i++]]);
}

其他三種方式性能差不多。需要根據情況選擇:

  • 根據每次迭代處理的事務
  • 根據迭代的次數

通過減少這兩者中的一個或全部的時間開銷,就能提升循環的整體性能。

減少迭代的工作量

循環複雜度爲O(n)時,減少迭代工作量是最有效的方法。

  • 獲取數組長度並緩存到局部變量中,避免每次迭代都獲取一次:
for(var i=0,len=items.length;i<len;i++){
    process(items[i]);
}

根據數組長度,在大部分瀏覽器中能節省大概25%的運行時間。

  • 倒序循環
for(var i=items.length;i--;){
    process(items[i]);
}

把減法操作放在控制條件中。現在每個控制條件只是簡單地與0比較。控制條件與true值比較時,任何非零數會自動轉換爲true,而0值等同於false。

現在控制條件已經從2次比較(迭代數小於總數嗎?它是否爲true?)減少到1次比較(它是true嗎?)。每次迭代從2次比較減少到1次,運行速度快了50%-60%。

減少迭代次數

複雜度大於O(n)時,建議着重減少迭代次數。

達夫設備:限制循環迭代次數的模式。是一個循環體展開技術,使得一次迭代中實際上執行了多次迭代的操作。

達夫設備的一個典型實現如下:

var iterations=Math.floor(items.length/8),
    startAt=items.length%8,
    i=0;
do{
    switch(startAt){
        case 0:process(items[i++]);
        case 7:process(items[i++]);
        case 6:process(items[i++]);
        case 5:process(items[i++]);
        case 4:process(items[i++]);
        case 3:process(items[i++]);
        case 2:process(items[i++]);
        case 1:process(items[i++]);
    }
    startAt = 0;
}while(--iterations);

基本理念:每次循環最多可調用8次process()。循環的迭代次數爲總數除以8.由於不是所有數字都能被8整除,變量startAt用來存放餘數,表示第一次循環中應調用多少次process()。如果是12次,那麼第一次循環會調用process()4次,第二次循環調用process()8次,用2次循環替代了12次循環。

此算法一個稍快的版本取消了switch語句,並將餘數處理和主循環分開:

var i=items.length%8;
while(i){
    process(items[i--]);
}
i=Math.floor(items.length/8);
while(i){
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
    process(items[i--]);
}

這種方式用2次循環代替之前的1次循環,但它移除了switch語句,速度比之前的更快。

但是達夫設備只在迭代數超過1000的情況下有明顯效率的提升。例如在500 000次迭代中,其運行時間比常規循環少70%。

3)基於函數的迭代

forEach()

使用方便,但比基於循環的迭代要慢一些。基於循環的迭代比基於函數的迭代快8倍。

性能消耗:對每個數組項調用外部方法帶來的開銷是速度慢的主要原因。

2.條件語句

1)if-else對比switch

基於測試條件的數量判斷使用哪種:條件數量越大,越傾向於使用switch而不是if-else。——基於代碼的易讀性。

事實證明,當條件數量很大時,switch比if-else運行得要快。

這兩個語句的性能區別是:當條件增加時,if-else性能負擔增加的程度比switch要多。——基於性能。

通常來說,if-else適用於判斷兩個離散值或幾個不同的值域。當判斷多於2個離散值時,switch語句更好。

2)優化if-else

優化目標:最小化到達正確分支前所需判斷的條件數量。

優化策略:

  • 最簡單方法:條件語句總是按照從最大概率到最小概率的順序排列,以確保運行速度最快。
  • 把if-else組織成一系列嵌套的if-else語句,會使運行速度平均化。

3)查找表

當有大量離散值需要測試時,if-else和switch比使用查找錶慢很多。

查找表方法:使用數組和普通對象來構建查找表。

在這裏插入圖片描述
優點:

  • 速度更快
  • 代碼可讀性更好,不用書寫任何條件判斷語句
  • 候選值數量增加時,幾乎不會產生額外的性能開銷

查找表示例:

var results=[result0,result1,result2,result3,result4,result5];
return results[value];

switch更適合於每個鍵都需要對應一個獨特的動作或一系列動作的情況。

3.遞歸

使用遞歸可以把複雜的算法變得簡單。

遞歸函數的潛在問題是終止條件不明確或缺少終止條件會導致函數長時間運行,並使得用戶界面處於假死狀態。還可能遇到瀏覽器的調用棧大小限制(Call stack size limites)。

1)調用棧限制

調用棧限制大小與瀏覽器關係:除IE外的瀏覽器都有固定數量的調用棧限制,IE的調用棧限制大小與系統空閒內存有關。大多數現代瀏覽器的調用棧限制大小比老版本瀏覽器高很多。

當遞歸次數超過最大調用棧容量時,瀏覽器會報告以下出錯信息

  • IE: Stack overflow at line x;同時彈出alert警告提示棧溢出
  • Firefox: Too much recursion
  • Safari: Maximum call stack size exceeded
  • Opera: Abort(control stack overflow)
  • Chrome: 不顯示

棧溢出可通過try-catch捕獲處理。

2)遞歸模式

2種遞歸模式:

  • 直接遞歸模式:1個函數遞歸
  • 隱伏模式:2個函數相互調用,行程無限循環,較難定位原因

隱伏模式示例:

//2個函數
function first(){
    second();
}
function second(){
    first();
}
first();

最常見的導致棧溢出的原因是不正確的終止條件。因此定位模式錯誤的第一步是驗證終止條件。如果終止條件沒問題,那麼可能是算法中包含了太多層遞歸。

建議改用迭代、Memoization。

3)迭代

任何遞歸能實現的算法,同樣可以用迭代來實現。迭代算法通常包含幾個不同的循環,這會引入自身的性能問題。

使用優化後的循環替代遞歸函數可以提升性能,因爲運行一個循環比反覆調用一個函數的開銷要少很多。

4)Memoization

是一種避免重複工作的方法,它緩存前一個計算結果供後續計算使用。

多次調用遞歸函數時,大量重複工作不可避免。

Memoization的關鍵在於在函數內部創建一個緩存對象,將緩存中沒有的計算結果放到緩存中,多次調用時已計算的結果就可以直接從緩存取出。

以下是封裝了基礎功能的memoize()函數:

function memoize(fundamental,cache){
    //fundamental爲需要增加緩存功能的原遞歸函數;cache爲可選的緩存對象
    cache=cache||{};
    var shell=function(arg){
        if(!cache.hasOwnProperty(arg)){
            cache[arg]=fundamental(arg);
        }
        return cache[arg];
    };
    return shell;
}

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

1.字符串連接

字符串合併的方法:

  • str=“a”+“b”;

  • str=“a”;

    str+=“b”;

  • str=[“a”,“b].join(”");

  • str=“a”;

    str=str.concat(“b”);

當連接少量字符串時,這些方法運行速度都很快。當需要合併的字符串長度和數量增加,有一些方法開始展現優勢。

1)+ 和 += 操作符

舉例:

str+="one"+"two";

此代碼運行時,會經歷4個步驟:

  1. 在內存中創建一個臨時字符串
  2. 連接後的字符串"onetwo"被賦值給該臨時字符串
  3. 臨時字符串與str當前的值連接
  4. 結果賦值給str

優化代碼1

用2行語句直接附加內容給str,避免了產生臨時字符串(原第1步和第2步)。大多數瀏覽器中這樣優化會提速10%-40%。

str+="one";
str+="two";

優化代碼2

賦值表達式由str開始作爲基礎,每次給它附加一個字符串,由左向右一次連接,因此避免了使用臨時字符串。

如果改變連接順序(如:str=“one”+str+“two”),本優化將會失效。這與瀏覽器合併字符串時分配內存的方法有關:

  • 除IE外,其他瀏覽器會嘗試爲表達式左側的字符串分配更多的內存,然後將第2個字符串拷貝至它的末尾。如果在一個循環中,基礎字符串位於最左端的位置,就可以避免重複拷貝一個逐漸變大的基礎字符串。

  • IE8與其他瀏覽器較相似。IE7及以下版本,使用優化代碼2會更慢。因爲在與一個唱的基礎字符串合併前,先連接多個短字符串會使速度更快(避免了多次複製大字符串)。

    例如largeStr=largeStr+s1+s2,IE7及以下版本中,必須將這個長字符串拷貝2次,1次是與s1合併,1次是與s2合併。相反,largeStr+=s1+s2,首先將2個小字符串合併起來,然後將結果再返回給長字符串。創建字符串s1+s2與兩次拷貝字符串相比,性能影響要小得多。

    這是由IE執行連接操作的底層機制決定的。

    • 在IE8中,連接字符串只是記錄現有的字符串的引用來構造新字符串。在最後時刻(調用時),字符串的各個部分纔會逐個拷貝到一個新的“真正的”字符串中,然後用它取代先前的字符串引用。所以並非每次使用字符串時都發生合併操作。
    • IE7及以下版本,使用了更糟糕的實現方法:每連接一對字符串都要把它複製到一塊新分配的內存中。

【基本字符串】:連接時排在前面的字符串。str+“one"意味着拷貝"one"並附加在str之後,而"one”+str則意味着要拷貝str並附加在"one"之後。如果str很大,則拷貝過程的內存佔用就大。

str=str+"one"+"two";
//等價於str+((str+"one")+"two");

2)Firefox和編譯期合併

在賦值表達式中所要鏈接的字符串都屬於編譯期常量。

Firefox會在編譯過程中自動合併編譯期常量。

str="one"+"two";在Firefox中會自動變成str="onetwo";

這種方式由於運行期沒有中間字符串,所以花在連接過程的時間和內存可以減少到0。但是這種做法不常用到,因爲更多時候是用運行期的數據構建字符串,而不是常量的拼接。

3)數組項合併

Array.prototype.join方法

大多數瀏覽器中,使用join合併比其他字符串連接方法更慢,但在IE7及以下版本中合併大量字符串的情況是高效的。

在IE7中使用join避免了重複分配內存和拷貝逐漸增大的字符串。當把數組所有元素連接在一起時,瀏覽器會分配足夠的內存來存放整個字符串,而且不會多次拷貝最終字符串中相同的部分。

4)String.prototype.concat

使用concat比+和+=稍慢,尤其是在IE、Opera和Chrome中更慢。有潛伏的災難性的性能問題(隨數據量的增大,具有指數級別倍數的慢速度)。

2.正則表達式優化

兩個正則表達式匹配相同的文本並不意味着有着相同的速度。部分匹配比完全不匹配所用的時間要長。不同瀏覽器對正則表達式引擎有着不同程度的內部優化。

1)正則表達式工作原理

處理一個正則表達式的基本步驟:

  1. 編譯:創建一個正則表達式對象(使用字面量或RegExp構造函數),瀏覽器會驗證你的表達式,然後把它轉化爲一個原生代碼程序,用於執行匹配工作。如果把正則對象賦值給一個變量,可以避免重複執行這一步驟。

  2. 設置起始位置:當正則類進入使用狀態,首先要確定目標字符串的起始搜索位置。這是字符串的起始字符,或者由正則表達式的lastIndex屬性指定。當嘗試匹配失敗時,此位置則在最後一次匹配的起始位置的下一位字符的位置上。

    瀏覽器優化正則的辦法是:提前決定跳過一些不必要的步驟,來避免大量無意義的工作。

  3. 匹配每個正則表達式字元:一旦正則表達式知道開始位置,它會逐個檢查文本和正則表達式模式。當一個特定的字元匹配失敗時,正則表達式會試着回溯到之前嘗試匹配的位置上,然後嘗試其他可能的路徑。

  4. 匹配成功或失敗:如果在字符串當前的位置發現了一個完全匹配,那麼正則表達式宣佈匹配成功。如果在所有可能的路徑都沒有匹配到,正則表達式引擎會回退到第二步,然後從下一個字符重新嘗試。當每個字符都經理這個過程,還沒有成功匹配,那麼就匹配失敗。

2)回溯

回溯是正則表達式匹配過程中的基礎組成部分。但是回溯會產生昂貴的計算消耗。理解它的工作原理以及如何最少化地使用它,是編寫高效正則的關鍵。

回溯過程概述:

  • 當正則匹配目標字符串時,它從左到右逐個測試表達式的組成部分,看是否能找到匹配項。

  • 在遇到量詞(比如*,+?或{2,0})和分支(|操作符)時,需要決策下一步如何處理。遇到量詞需決定何時嘗試匹配更多字符;遇到分支則必須從可選項中選擇一個嘗試匹配。

  • 每當正則表達式做類似的決定時,如果有必要都會記錄其他選擇,以備返回時使用。

  • 如果當前選項匹配成功,正則表達式繼續掃描表達式;如果其他部分也匹配成功,那麼匹配結束。

  • 但是,如果當前選項找不到匹配值,或後面的部分匹配失敗,那麼正則表達式會回溯到最後一個決策點,然後在剩餘的選項中選擇一個;這個過程會一直進行,直到找到匹配項或者正則表達式中量詞和分支選項的所有排列組合都嘗試失敗,那麼它將放棄匹配,轉而移動到字符串中的下一個字符,再重複此過程。

3)回溯失控

當正則表達式導致瀏覽器假死數秒、數分鐘、甚至更長時間,問題很可能是回溯失控。

是由過多貪婪量詞(.*?[\s\S]*?)導致的反覆回溯。

解決方案:儘可能具體化分隔符之間的字符串匹配模式。比如改爲".*?"用來匹配由雙引號包圍的字符串。

匹配HTML字符串

匹配HTML字符串時,可以通過重複一個非捕獲組來實現,它包含了否定性預查(阻止下一個依賴標籤)和[\s\S](任意字符)元序列。非捕獲組例如=>(?:(?!<head>)[\s\S])*<head>

這樣消除了潛在的回溯失控,並允許正則表達式在匹配不完整的HTML字符串失敗時所需要時間通字符串長度成線性關係。但是爲每個匹配字符重複預查是嚴重缺乏效率的。這種方法在匹配短字符串時運行良好。

使用預查和反向引用的模擬原子組

原子組:(?>…),其中…表示任意正則表達式。它是一種具有特殊反轉性的非捕獲組。一個原子組中存在一個正則,該組的任何回溯位置都會被丟棄。可以有效阻止海量回溯。

模擬原子組:js不支持原子組,可以利用預查((?=...))過程來模擬。預查作爲全局匹配的一部分,並不消耗任何字符;只是檢查自己包含的正則符號在當前字符串位置是否匹配。可以通過把預查的表達式封裝在捕獲組中並給它添加一個反向引用的方法來避免這一問題。

模擬原子組示例:

(?=(...))\1

匹配HTML字符串應用:(?=([\s\S]*?<head>))\1

嵌套量詞和回溯失控

嵌套量詞是指量詞出現在一個自身被重複量詞修飾的組中,例如(x+)*

4)基準測試的說明

建議總是用包含特殊匹配的長字符串來測試正則表達式。

5)更多提高正則表達式效率的方法

  • 關注如何讓匹配更快失敗:因爲正則匹配失敗的位置比匹配成功的位置要多得多。
  • 正則表達式以簡單、必需的字元開始:起始標記應儘可能快速地測試並排除明顯不匹配的位置。
  • 使用量詞模式,是它們後面的字元互斥:儘量具體化匹配模式。
  • 減少分支數量,縮小分支範圍:可以通過字符集合選項組件來減少對分支的需求;當分支必不可少時,將常用分支放到最前面。
  • 使用非捕獲組:捕獲組消耗時間和內存來記錄反向引用。
  • 只捕獲感興趣的文本以減少後處理。
  • 暴露必需的字元。
  • 使用合適的量詞。
  • 把正則表達式賦值給變量並重用它們:可以避免對正則重新編譯。
  • 將複雜的正則拆分爲簡單的片段。

6)何時不使用正則

字符串方法(charAt/slice/substr/substring)都可用在特定位置上提取並檢查字符串的值。

indexOf和lastIndexOf方法非常適合查找特定字符串的位置,或判斷是否存在。

所有的字符串方法速度都很快,當搜索那些並不依賴正則複雜特性的字面字符串時,字符串方法有助於避免正則帶來的性能開銷。

3.去除字符串收尾空白

trim()

1)使用正則表達式去首尾空白

使用2個子表達式:去除頭部的空白、去除尾部的空白。

if(!String.prototype.trim){
    String.prototype.trim=function(){
        return this.replace(/^\s+/,"").replace(/\s+$/,"");
    }
}

2)不使用正則表達式去除字符串首尾空白

String.prototype.trim=function(){
    var start=0,
        end=this.length-1,
        ws="\n\r\t\f\xob\xa0\u168o\u18oe\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff";
    while(ws.indexOf(this.charAt(start))>-1){
        start++;
    }
    while(end>start&&ws.indexOf(this.charAt(end))>-1){
        end--;
    }
    return this.slice(start,end+1);
}

ws變量包含了es5定義的所有空白字符。

缺點:不宜用來處理前後大段的空白字符。因爲通過循環遍歷字符串來確定空白字符的效率比不上正則表達式使用的優化後的搜索代碼。

3)混合解決方案

用正則過濾頭部空白,用非正則方法過濾尾部字符。

String.prototype.trim=function(){
    var str=this.replace(/^\s+/,""),
        end=str.length-1,
        ws=/\s/;
    while(ws.test(str.charAt(end))){
        end--;
    }
    return str.slice(0,end+1);
}

第6章 快速響應的用戶界面

瀏覽器讓一個單線程共用於執行js和更新用戶界面。每個時刻只能執行其中一種操作,當js代碼執行時,用戶界面處於“鎖定”狀態。

1.瀏覽器UI線程

瀏覽器UI線程:用於執行JS和更新用戶界面的進程。

UI線程的工作基於一個簡單的隊列系統,任務會被保存到隊列中直到進程空閒。一旦空閒,隊列中的下一個任務就被重新提取出來並運行。這些任務要麼是運行js代碼,要麼是執行UI更新,包括重繪和重排。

大多數瀏覽器在js運行時會停止把新任務加入UI線程的隊列中。

1)瀏覽器限制

瀏覽器限制分2種:調用棧大小限制和長時間運行腳本限制。

長時間運行腳本限制基本原理是瀏覽器會記錄一個腳本的運行時間,並在達到一定限度時終止它。

2種方法可以度量腳本運行時間:

  • 記錄自腳本開始以來執行語句的數量。
  • 記錄腳本執行的總時長。

不同瀏覽器檢測方法和具體限制大小不同:

  • IE4及以上,設置默認限制爲500萬條語句;此限制放在Windows註冊表中,叫做HKEY_CURRENT_USER\Software\Microsoft\InternetExplorer\Styles\MaxScriptStatements。
  • Firefox的默認限制爲10秒;該限制記錄在瀏覽器配置設置中,鍵名爲dom.max_script_time。
  • Safari的默認限制爲5秒;該限制無法更改,但是可以通過Develop菜單選擇Disable Runaway JavaScript Timer來禁用定時器。
  • Chrome沒有單獨的長運行腳本限制,替代做法是依賴其通用崩潰檢測系統來處理此類問題。
  • Opera沒有長運行腳本限制,它會繼續執行js代碼直到結束,鑑於Opera架構,腳本運行結束時不會導致系統不穩定。

當長運行腳本限制被觸發時,瀏覽器會彈出對話框提示用戶。這個代碼無法檢測,也不能改變對話框外觀,所以只能儘量避免該問題發生。

2)多久算長時間

單個js操作花費的總時間不應該超過100毫秒。

Nielsen指出如果界面在100毫秒內響應用戶輸入,用戶會認爲自己在“直接操縱界面中的對象”。超過100毫秒意味着用戶會感到自己與界面失去聯繫。

建議不要讓任何js代碼持續運行50毫秒以上。

2.使用定時器讓出時間片段

難免會有一些複雜的js任務不能在100毫秒內完成,最理想的方法是讓出UI線程的控制權,使得UI可以更新。

1)定時器基礎

setTimeout() setInterval()

方法有2個參數,第一個是函數,第2個是毫秒數。但第二個時間表示任務何時被添加到UI隊列,而不是一定會在這段時間後執行,這個任務會等待隊列中其他所有任務執行完畢纔會執行。

無論發生何種情況,創建一個定時器會造成UI線程暫停。因此,定時器代碼會重置所有相關的瀏覽器限制,包括長時間運行腳本定時器。此外,調用棧也在定時器的代碼中重置爲0。這一特性使得定時器成爲長時間運行js代碼理想的跨瀏覽器解決方案。

2)定時器精度

js定時器延遲通常不太精準,相差大約幾毫秒。指定定時器延時250毫秒,並不意味着任務會在調用setTimeout之後過250毫秒時精準地加入隊列。瀏覽器都會發生幾毫秒偏移,或快或慢。因此,定時器不可用於測量實際時間。

在Windows系統中定時器分辨率爲15毫秒,也就是說一個延時15毫秒的定時器將根據最後一次系統時間刷新而轉換爲0或15。設置定時器延時小於15將會導致IE鎖定,所以延遲的最小值建議爲25毫秒(實際時間是15或30),以確保至少有15毫秒延遲。

定時器延時的最小值有助於避免在其他瀏覽器和其他操作系統中的定時器出現分辨率問題。大多數瀏覽器在定時器延時等於或小於10毫秒時表現不太一致。

3)使用定時器處理數組

常見的一種長運行腳本的起因是耗時過長的循環。可以使用定時器優化——基本方法是把循環的工作分解到一系列定時器中。

影響循環性能的2部分:數組長度長;處理語句複雜。

決定是否可以使用定時器取代循環的2個因素:

  • 處理過程是否必須同步?
  • 數據是否必須按順序處理?

如果都是否,那麼可以使用定時器取代。

//原循環
for(var i=0,len=items.length;i<len;i++){
    process(items[i]);
}
//用異步取代循環,封裝一個函數
function processArray(items,process,callback){
	var todo=items.concat();//克隆原數組
	setTimeout(function(){
    	process(todo.shift());
    	//如果還有需要處理的元素,創建另一個定時器
    	if(todo.length>0){
        	setTimeout(arguments.callee, 25);//因爲下一個定時器需要運行相同的代碼,所以第一個參數爲arguments.callee,該值指向當前正在運行的匿名函數
    	}else{
        	callback(items);
    	}
	},25);
}

使用定時器取代循環缺點:處理數組的總時長增加了。這是因爲在每一個條目處理完成後UI線程會空閒出來,並且在下一條目開始處理之前會有一段延時。

儘管如此,爲避免鎖定瀏覽器給用戶帶來的糟糕體驗,這種取捨是有必要的。

4)分割任務

如果一個函數運行時間太長,可以把它拆分成一系列更小的步驟,把每個獨立的方法放在定時器中調用。

5)記錄代碼運行時間

批量處理比單個處理要快。

可以通過原生的Date對象來跟蹤代碼的運行時間,這是大多數js分析工具的工作原理。

通過添加一個時間監測機制來改進processArray()方法,使得每個定時器能處理多個數組條目:

function timedProcessArray(items,process,callback){
    var todo=items.concat();//克隆原數組
	setTimeout(function(){
        var start=+new Date();
        //每個數組條目處理完後檢測執行時間
        do{
            process(todo.shift());
        }while(todo.length>0&&(+new Date()-start<50));

    	//如果還有需要處理的元素,創建另一個定時器
    	if(todo.length>0){
        	setTimeout(arguments.callee, 25);
    	}else{
        	callback(items);
    	}
	},25);
}

使用50毫秒內的任務批量處理,能避免把任務分解成過於零碎的片段。

6)定時器與性能

過度使用定時器會對性能造成負面影響。以上小節的代碼使用了定時器序列,同一時間只有一個定時器存在,只有當這個定時器結束時纔會新創建一個。這種方法使用定時器不會導致性能問題。

當多個重複的定時器同時創建往往會出現性能問題。因爲只有一個UI線程,而所有的定時器都在爭奪運行時間。那些間隔在1秒或以上的低頻率重複定時器幾乎不會影響Web應用響應速度;而多個重複定時器使用較高的頻率(100-200毫秒之間)時,應用會明顯變慢。

3.Web Workers

Web Workers給Web應用帶來潛在的巨大性能提升,因爲每個Worker都在自己的線程中運行代碼。這意味着Worker運行代碼不僅不會影響瀏覽器UI,也不會影響其他Worker中運行的代碼。

1)Worker運行環境

由於Web Workers沒有綁定UI線程,它們不能訪問瀏覽器的資源。

Web Workers從外部線程中修改DOM會導致用戶界面出現錯誤。

每個Web Worker都有自己的全局運行環境,由如下部分組成:

  • navigator對象,包括4個屬性:appName、appVersion、userAgent、platform。
  • location對象,與window.location相同,但所有屬性都是隻讀的。
  • self對象,指向全局worker對象。
  • importScripts()方法:加載Worker所用到的外部js文件。
  • 所有js對象,如Object、Array、Date等。
  • XMLHttpRequest構造器。
  • setTimeout()和setInterval()方法。
  • close()方法:能立刻停止Worker運行。

需要創建一個完全獨立的js文件,其中包含了需要在Worker中運行的代碼。創建Web Worker必須傳入這個js文件的url:var worker=new Worker("code.js");

此代碼一旦執行,將爲這個文件創建一個新的線程和一個新的Worker運行環境。該文件會被異步下載,直到文件下載並執行完成後纔會啓動此Worker。

2)與Worker通信

Worker與網頁代碼通過事件接口進行通信。網頁代碼可以通過postMessage()方法給Worker傳遞數據,它接受一個參數,即需要傳遞給Worker的數據。此外,Worker還有一個用來接受信息的onmessage事件處理器。

消息系統是網頁和Worker通信的唯一途徑。

3)加載外部文件

Worker通過importScripts()方法加載外部js文件,該方法接收一個或多個js文件的url作爲參數,該方法的調用過程是阻塞式的,直到所有文件加載並執行完成後,腳本纔會繼續運行。由於Worker在UI線程之外運行,所以這種阻塞並不會影響UI響應。

4)實際應用

Web Workers適用於處理純數據,或者與瀏覽器UI無關的長時間運行腳本。

可能受益的情況:

  • 編碼/解碼大字符串
  • 複雜數學運算(包括圖像或視頻處理)
  • 大數組排序

任何超過100毫秒的處理過程,都應當考慮Worker方案是不是比基於定時器的方案更爲合適,前提是瀏覽器支持Web Workers。

第7章 Ajax

ajax可以通過延遲下載資源文件使頁面加載速度更快。通過異步的方式在客戶端和服務端之間傳輸數據,從而避免頁面資源一窩蜂地下載。選擇適合的傳輸方式和數據格式,可以顯著改善用戶和網站的交互體驗。

1.數據傳輸

ajax是一種與服務器通信而無需重載頁面的方法。

1)請求數據

有5種常用技術用於請求:

  • XHR
  • 動態腳本注入
  • iframes
  • Comet
  • Multipart XHR

現代高性能js中使用的3種技術是:XHR、動態腳本注入、Multipart XHR。

HTTP請求是ajax中最大的瓶頸之一,因此減少http請求數量能提高頁面性能。

2)發送數據

當數據只需要發送到服務器時,有2種廣泛使用的技術:XHR和信標。

使用XHR發送數據到服務器時,GET方式會更快,因爲對於少量數據而言,一個GET請求網服務器只發送一個數據包,而一個POST請求至少發送2個數據包,一個裝載頭信息,另一個裝載POST正文。

信標(Beacons):類似動態腳本注入。可以使用圖片信標,創建一個img元素但不插入頁面DOM,給img的src設置爲請求服務器上腳本的URL,URL上可以傳參。

信標優點:無需向客戶端發送任何回饋信息;性能消耗小;服務端的錯誤不會影響到客戶端。

信標缺點:無法發送POST數據;發送的數據長度被限制得很小。

選擇數據傳輸技術需考慮的因素:功能集、兼容性、性能、方向。

2.數據格式

考慮數據格式時,唯一需要比較的標準就是速度。

1)XML

優點:極佳的通用性、格式嚴格易於驗證。

缺點:冗長,每個單獨的數據片段都依賴大量結構,所以有效數據的比例非常低;js程序手動解析複雜。

2)JSON

JSON是一種使用js對象和數組直接量編寫的輕量級且易於解析的數據格式。

JSON-P:在使用動態腳本注入時,JSON數據被當成一個js文件並作爲原生代碼執行。爲實現這一點,這些數據必須封裝在一個回調函數裏。這就是JSON填充(JSON with padding)或 JSON-P。

優點:體積小;在相應信息中結構所佔的比例更小,數據佔用比例更多;極好的通用性;客戶端解析速度快。

3)HTML

可以在服務端構建好整個HTML再傳回客戶端,js可以通過innerHTML屬性把HTML插入頁面相應位置。

缺點:數據格式臃腫;傳輸數據量大;客戶端解析速度慢。

當客戶端的瓶頸是CPU而不是帶寬時可以使用此數據格式。

4)自定義格式

把數據用分隔符連接起來,返回一個字符串,js用split分割。

優點:快速下載;易於解析;格式簡潔,數據佔比高。

3.Ajax性能指南

除數據傳輸技術和數據格式以外的其他優化技術。

1)緩存數據

2種方法避免發送不必要的請求:

  • 在服務端,設置HTTP頭信息以確保你的響應會被瀏覽器緩存
  • 在客戶端,把獲取到的信息存儲到本地
設置HTTP頭信息

使用GET方式,設置Expires頭信息。

本地數據存儲

前端代碼請求時,若緩存中無對應數據則請求,請求後保存到全局變量localCache中,若緩存中已有則從緩存中獲取數據。

2)Ajax類庫的侷限

類庫一般不提供readystatechange事件,無法使用通過監聽readyState爲3時使用multipart XHR。

第8章 編程實踐

1.避免雙重求值

當你在js代碼中執行另一段js代碼時,都會導致雙重求值的性能消耗。此代碼首先會以正常的方式求值,然後在執行過程中對包含於字符串中的代碼發起另一個求值運算。

使用eval()、Funtion()、setTimeout()、setInterval()時都要創建一個新的解釋器/編譯器實例。這使得消耗很大,代碼執行速度變慢。

2.使用Object/Array直接量

直接量比new Object()new Array()運行得更快,並且可以節省代碼量。

3.避免重複工作

最常見的重複工作就是瀏覽器探測。

有幾種方法可以避免。

1)延遲加載

延遲加載意味着在信息被使用前不會做任何操作。

在第一次調用時,會先檢查瀏覽器兼容性並決定使用哪個api,然後用新函數覆蓋原函數。這樣的話隨後每次調用都不會在做瀏覽器檢測。

當一個函數在頁面中不會立刻調用時,延遲加載是最好的選擇。

2)條件預加載

條件預加載會在腳本加載期間提前檢測,而不會等到函數被調用。檢測的操作依然只有一次。

預加載適用於一個函數馬上就要被用到,並且在整個頁面的生命週期中頻繁出現。

4.使用速度快的部分

js引擎是由低級語言構建的而且經過編譯,所以引擎是很快的。但js代碼運行速度慢。引擎的某些部分允許你繞過那些慢的部分。

1)位操作

js中的數字都依照IEEE-754標準以64位格式存儲。在位操作中,數字被轉換爲有符號的32位格式。每次運算符會直接操作該32位數以得到結果。儘管需要轉換,但這個過程與js其他數學運算和布爾操作相比要快很多。

按位與、按位或、按位異或、按位取反

有好幾種方法利用位操作符提升js的速度——使用位運算代替純數學操作:

  1. 比如通常採用對2取模運算實現表格行顏色交替:
//原寫法
for(var i=0,len=rows.length;i<len;i++){
    if(i%2){
        className="even";
    }else{
        className="odd";
    }
    //增加class
}
//使用位操作符優化
for(var i=0,len=rows.length;i<len;i++){
    if(i&1){
        className="odd";
    }else{
        className="even";
    }
    //增加class
}

32位數字的二進制底層表示,可以發現偶數的最低位是0,奇數的最低位是1。可以簡單地通過讓給定數與數字1進行按位與運算判斷。當次數爲偶數,那麼它和1進行按位與運算結果是0,如果此數爲奇數,那麼它和1進行按位與運算的結果就是1。

雖然代碼改動不大,但優化後比原來快了50%。

  1. 使用位掩碼

位掩碼用於處理同時存在多個布爾選項的情形。其思路:使用單個數字的每一位來判定是否選項成立,從而有效地把數字轉換爲由布爾值標記組成的數組。掩碼中的每個選項的值都等於2的冪。

舉例:

var OPTION_A=1;
var OPTION_B=2;
var OPTION_C=4;
var OPTION_D=8;
var OPTION_E=16;

var options=OPTION_A|OPTION_C|OPTION_D;
//接下來可以通過按位與操作來判斷一個給定的選項是否可用:如果該選項未設置則運算結果爲0;如果已設置則結果爲1
if(options&OPTION_A){//選項A是否在列表中
    //do sth.
}
if(options&OPTION_B){//選項B是否在列表中
    //do sth.
}

這樣的位掩碼運算速度非常快,原因是計算操作發生在系統底層。如果有許多選項保存在一起並頻繁檢查,位掩碼有助於提高整體性能。

2)原生方法

無論js代碼如何優化,都永遠不會比js引擎提供的原生方法更快。因爲js的原聲部分在你寫代碼前已經存在瀏覽器中了,並且都使用低級語言寫的。這意味着這些原生方法會被編譯成機器碼,成爲瀏覽器的一部分。

  1. 複雜的數學運算使用原生的Math對象性能更好。Math對象包含一些常見的數學常量和api。
  2. jquery引擎被廣泛認爲是最快的css查詢引擎,但是它仍然比原生方法慢。原生的querySelector()和querySelectorAll()方法完成任務平均所需時間是基於js的css查詢的10%。

當原生方法可用時儘量使用它們,特別是數學運算和DOM操作。

第9章 構建並部署高性能js應用

1.Apache Ant

Apache Ant是一個軟件構建自動化工具。用Java實現並用XML描述構建過程。

2.Gulp

比起Apache ant,Gulp更爲流行。

3.合併多個js文件

4.預處理js文件

5.js壓縮

js壓縮是指把js文件中所有與運行無關的部分進行剝離的過程。玻璃的內容包括註釋和不必要的空白字符。該過程通常可以將文件大小減半,促使文件更快被下載。

YUI Compressor提供了更高級別的壓縮,除了壓縮註釋和不必要的空格,還提供如下功能:

  • 將局部變量替換成更短的形式(1個、2個或3個字符),以優化後續的Gzip壓縮工作。
  • 儘可能將方括號表示法替換爲點表示法(如foo['bar']被替換成foo.bar)。
  • 儘可能去掉直接量屬性名的引號(如{"foo":"bar"}被替換成{foo:"bar"})。
  • 替換字符串中的轉義符號(如'aaa\'bbb'被替換成"aaa'bbb")。
  • 合併常量(如"foo"+"bar"被替換成"foobar")。

6.構建時處理對比運行時處理

開發高性能應用的一個普遍規則是,只要是能在構建時完成的工作,就不要留到運行時去做。

7.js的HTTP壓縮

Accept-Encoding頭信息用於告訴Web服務器它支持哪種編碼轉換類型。這個信息主要用來壓縮文檔以獲得更快的下載。可用的值包括:gzip、compress、deflate、identity。

如果Web服務器在請求中看到這些信息頭,它會選擇最適合的編碼方法,並通過Content-Encoding頭信息通知Web瀏覽器它的決定。

gzip是目前最流行的編碼方式,它通常能減少70%的下載量。gzip壓縮主要適用於文本(包括js文件)。其他類型比如圖片、pdf文件,不應該使用gzip,因爲他們本身已經被壓縮過,試圖重複壓縮只會浪費服務器資源。

8.緩存js文件

  • Web服務器通過“Expires HTTP響應頭”來告訴客戶端一個資源應當緩存多長時間。

  • 使用HTML5離線應用緩存。

9.處理緩存問題

適當的緩存控制能切實提升用戶體驗,但有個缺點,當應用升級時,你需要確保用戶下載到最新的靜態內容。這個問題可以通過把改動過的靜態資源重命名來解決。

10.使用內容分發網絡(CDN)

CDN是互聯網上按地理位置分佈計算機網絡,它負責傳遞內容給終端用戶。使用CDN的主要原因是增強Web應用的可靠性、可擴展性,更重要的是提升性能。事實上,通過地理位置最近的用戶傳輸內容,CDN能極大地減少網絡延時。

11.部署js資源

部署js資源的過程通常需要複製大量文件到一臺或多臺遠程主機,有時還需要在遠程主機上執行一系列的shell命令,尤其是使用CDN時,需要通過傳輸網絡分發新添加的文件。

Apache Ant提供幾個選項用於複製文件到遠程主機。

爲了在運行SSH守護進程的遠程主機上執行shell命令,可以使用可選的SSHEXEC任務或直接調用ssh工具。

12.敏捷js構建過程

傳統的build工具很優秀,但是每一次變更代碼後都必須手動編譯。Web開發人員更中意的方案是跳過編譯的步驟,只需刷新瀏覽器窗口即可。

smasher是個php5編寫的結合上述先進技術的工具,能幫助Web開發人員在高效工作的同事依然能獲得應用最佳性能。

第10章 工具

性能分析:在腳本運行期間定時執行各種函數和操作,找出需要優化的部分。

網絡分析:檢查圖片、樣式表和腳本的加載過程,以及它們對頁面整體加載和渲染的影響。

1.js性能分析

使用Date對象測量腳本的任何部分。

比起手動插入計時代碼,一個能處理時間計算並存儲數據的Timer對象會是更好的方案:

var Timer={
    _data:{},
    start:function(key){
        Timer._data[key]=new Date();
    },
    stop:function(key){
        var time=Timer._data[key];
        if(time){
            Timer._data[key]=new Date()-time;
        }
    },
    getTime:function(key){
        return Timer._data[key];
    }
};

//使用
Timer.start('createElement');
for(i=0;i<count;i++){
    element=document.createElement('div');
}
Timer.stop('createElement');
alert(Timer.getTime('createElement'));

2.YUI Profiler

是一個用js編寫的js性能分析工具。除了計時功能,還提供了針對函數、對象和構造器的性能分析接口,並提供性能分析數據的詳細報告。可以跨瀏覽器工作。

3.匿名函數

使用匿名函數或賦值函數可能會造成分析工具的數據變的混亂。分析匿名函數的最佳辦法是給它們取個名字。使用指針指向對象的方法而不是使用閉包,可以實現最充分的分析覆蓋率。

4.Firebug

1)控制檯面板分析工具

Profile功能:

  • Calls表示函數被調用的次數。
  • OwnTime表示函數自身消耗的時間。
  • Time表示函數以及被它調用的函數所消耗的總時間。

2)Console API

Firebug提供了一個啓動或停止性能分析的js接口:console.profile()和console.profileEnd()。

profileEnd會花費時間來生成報告,因此會增加腳本的開銷。最好的方法是放在setTimeout中執行。

3)網絡面板

這個面板以可視化的方式展示了腳本對其他資源的阻塞作用,可以深入探查腳本對其他文件加載造成的影響,以及對頁面的一般影響。

第一條藍色的垂直線,表示頁面DOMContentLoaded事件觸發的時刻,這個事件標誌着頁面的DOM樹已經解析完成並準備就緒。

第二條紅色的垂直線,表示window的load事件觸發的時刻,意味着DOM準備就緒後所有外部資源也已加載完成。

5.IE開發人員工具

IE分析工具包括了函數分析,並提供一個包括調用次數、消耗時間以及其他性能數據的詳細報告。該報告能以調用樹的方式查看,還能分析原生函數,並導出分析數據。

6.Safari Web檢查器

7.Chrome開發人員工具

Timeline面板提供了所有活動的概況,按分類可分爲:Loading、Scripting、Rendering。

8.腳本阻塞

傳統上,瀏覽器限制每次只能發出一個腳本請求,這樣做是爲了管理文件之間的依賴關係。只要一個文件依賴於在源碼中靠後的另一個文件,那麼它所依賴的那個文件必須保證在它運行之前準備就緒。腳本之間存在間隙就說明腳本被阻塞了。

新版瀏覽器解決這個問題的方法是允許並行下載,但阻塞運行,以確保依賴關係已經準備好。雖然這樣能使文件下載得更快,但頁面渲染仍然會被阻塞,直到所有腳本都被執行。

9.Page Speed

類似Firebug的網絡面板,提供了Web頁面加載的資源的信息。除了加載時間和http狀態,他還能顯示解析和運行js消耗的時間,指明可延遲加載的腳本,並報告那些沒有被使用的函數。

10.Fiddler

一個http調試代理工具。

11.YSlow

YSlow工具可以深入觀察頁面初始加載和運行過程的整體性能。

12.dynaTrace Ajax Edition

是一個強大的Java/.NET性能診斷工具。提供了一個全面的性能分析,從網絡和頁面渲染到運行器腳本和CPU使用率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章