《高性能javascript》閱讀摘要

最近在閱讀這本Nicholas C.Zakas(javascript高級程序設計做者)寫的最佳實踐、性能優化類的書。記錄下主要知識。


加載和執行

腳本位置

放在<head>中的javascript文件會阻塞頁面渲染:通常來講瀏覽器中有多種線程:UI渲染線程、javascript引擎線程、瀏覽器事件觸發線程、HTTP請求線程等。多線程之間會共享運行資源,瀏覽器的js會操做dom,影響渲染,因此js引擎線程和UI渲染線程是互斥的,致使執行js時會阻塞頁面的渲染。
最佳實踐:全部的script標籤應儘量的放在body標籤的底部,以儘可能減小對整個頁面下載的影響。javascript

組織腳本

每一個<script>標籤初始下載時都會阻塞頁面渲染,因此應減小頁面包含的<script>標籤數量。內嵌腳本放在引用外鏈樣式表的<link>標籤以後會致使頁面阻塞去等待樣式表的下載,建議不要把內嵌腳本緊跟在<link>標籤以後。外鏈javascript的HTTP請求還會帶來額外的性能開銷,減小腳本文件的數量將會改善性能。php

無阻塞的腳本

無阻塞腳本的意義在於在頁面加載完成後才加載javascript代碼。(window對象的load事件觸發後)css

延遲的腳本

帶有defer屬性的<script>標籤能夠放置在文檔的任何位置。對應的javascript文件將在頁面解析到<script>標籤時開始下載,但並不會執行,直到DOM加載完成(onload事件被觸發前)。當一個帶有defer屬性的javascript文件下載時,它不會阻塞瀏覽器的其餘進程,能夠與其餘資源並行下載。執行的順序是script、defer、load。html

動態腳本元素

使用javascript動態建立HTML中script元素,例如一些懶加載庫。
優勢:動態腳本加載憑藉着它在跨瀏覽器兼容性和易用的有時,成爲最通用的無阻塞加載解決方式。前端

XHR腳本注入

建立XHR對線個,用它下載javascript文件,經過動態建立script元素將代碼注入頁面中java

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);

優勢:能夠下載javascript但不當即執行,在全部主流瀏覽器中均可以正常工做。
缺點:javascript文件必須與所請求的頁面處於相同的域,意味着不能文件不能從CDN下載。
node


數據存取

存儲的位置

數據存儲的位置會很大程度上影響讀取速度。c++

  • 字面量:字面量只表明自身,不存儲在特定的位置。包括:字符串、數字、布爾值、對象、數組、函數、正則表達式、null、undefined。(我的理解:對象的指針自己是字面量)
  • 本地變量:var定義的數據存儲單元。
  • 數組元素:存儲在javascript數組內部,以數字爲引。
  • 對象成員:存儲在javascript對象內部,以字符串做爲索引。

大多數狀況下從一個字面量和一個局部變量中存取數據的差距是微不足道的。訪問數據元素和對象成員的代價則高一點。若是在意運行速度,儘可能使用字面量和局部變量,減小數組和對象成員的使用。程序員

管理做用域

做用域鏈

每一個javascript函數都表示爲一個對象,更確切的說是Function對象的一個實例。它也有僅供javascript引擎存儲的內部屬性,其中一個內部屬性是[[Scope]],包含了一個被建立的做用域中對象的集合即做用域鏈。做用域鏈決定哪些數據能被函數訪問。做用域中的每一個對象被稱爲一個可變對象。
當一個函數被建立後,做用域鏈會被建立函數的做用域中可訪問的數據對象所填充。執行函數時會建立一個稱爲執行上下文的內部對象。執行上下文定義了函數執行時的環境。每次函數執行時對應的執行環境都是獨一無二的,屢次調用同一個函數也會建立多個執行上下文,當函數執行完畢,執行上下文就會被銷燬。每一個執行上下文都有本身的做用域鏈,用於解析標識符。當執行上下文被建立時,它的做用域鏈初始化爲當前運行函數的[[Scope]]屬性中的對象。這些值按照它們出如今函數中的順序,被複制到執行環境的做用域鏈中。這個過程一旦完成,一個被稱爲活動對象的新對象就爲執行上下文建立好了。
活動對象做爲函數運行時的變量對象,包含了全部局部對象,命名函數,參數集合以及this。而後此對象被推入做用域鏈的最前端。當執行環境被銷燬時,活動對象也隨之銷燬。執行過程當中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪裏獲取或存儲數據。該過程搜索執行環境的做用域鏈,查找同名的標識符。搜索過程從做用域鏈頭部開始,也就是當前運行函數的活動對象。若是找到,就使用這個標識符對應的變量,若是沒找到,繼續搜索做用域鏈的下一個對象知道找到,若沒法搜索到匹配的對象,則標識符被看成未定義的。這個搜索過程影響了性能。web

標識符解析的性能

一個標識符所在的位置越深,讀寫速度就越慢,全局變量老是存在於執行環境做用域的最末端,所以它是最深的。
最佳實踐:若是某個跨做用域的值在函數中被引用一次以上,那麼就把它存儲到局部變量中。

改變做用域鏈

通常來講一個執行上下文的做用域鏈是不會改變的。可是,with語句和try-catch語句的catch子語句能夠改變做用域鏈。
with語句用來給對象的全部屬性建立一個變量,能夠避免屢次書寫。可是存在性能問題:代碼執行到with語句時,執行環境的做用域鏈臨時被改變了,建立了一個新的(包含了with對象全部屬性)對象被建立了,以前全部的局部變量如今處於第二個做用域鏈對象中,提升了訪問的代價。建議放棄使用with語句。
try-catch語句中的catch子句也能夠改變做用域鏈,當try代碼塊中發生錯誤,執行過程會自動跳轉到catch子句,把異常對象推入一個變量對象並置於做用域的首位,局部變量處於第二個做用域鏈對象中。簡化代碼可使catch子句對性能的影響下降。
最佳實踐:將錯誤委託給一個函數來處理。

動態做用域

不管with語句仍是try-catch語句的子句catch子句、eval()語句,都被認爲是動態做用域。通過優化的javascript引擎,嘗試經過分析代碼來肯定哪些變量是能夠在特定的時候被訪問,避開了傳統的做用域鏈,取代以標識符索引的方式快速查找。當涉及動態做用域時,這種優化方式就失效了。
最佳實踐:只在確實有必要時使用動態做用域。

閉包、做用域和內存

因爲閉包的[[Scope]]屬性包含了與執行上下文做用域鏈相同的對象的引用,所以會產生反作用。一般來講,函數的活動對象會隨着執行環境一同銷燬。但引入閉包時,因爲引用仍然存在閉包的[[Scope]]屬性中,所以激活對象沒法被銷燬,致使更多的內存開銷。

最須要關注的性能點:閉包頻繁訪問跨做用域的標識符,每次訪問都會帶來性能損失。

最佳實踐:將經常使用的跨做用域變量存儲在局部變量中,而後直接訪問局部變量。

對象成員

不管是經過建立自定義對象仍是使用內置對象都會致使頻繁的訪問對象成員。

原型

javascript中的對象是基於原型的。解析對象成員的過程與解析變量十分類似,會從對象的實例開始,若是實例中沒有,會一直沿着原型鏈向上搜索,直到找到或者到原型鏈的盡頭。對象在原型鏈中位置越深,找到它也就越慢。搜索實例成員比從字面量或局部變量中讀取數據代價更高,再加上遍歷原型鏈帶來的開銷,這讓性能問題更爲嚴重。

嵌套成員

對象成員可能包含其餘成員,每次遇到點操做符"."會致使javascript引擎搜索全部對象成員。

緩存對象成員值

因爲全部相似的性能問題都與對象成員有關,所以應該儘量避免使用他們,只在必要時使用對象成員,例如,在同一個函數中沒有必要屢次讀取同一個對象屬性(保存到局部變量中),除非它的值變了。這種方法不推薦用於對象的方法,由於將對象方法保存在局部變量中會致使this綁定到window,致使javascript引擎沒法正確的解析它的對象成員,進而致使程序出錯。


DOM編程

瀏覽器中的DOM

文檔對象模型(DOM)是一個獨立於語言的,用於操做XML和HTML文檔的程序接口API。DOM是個與語言無關的API,在瀏覽器中的接口是用javascript實現的。客戶端腳本編程大多數時候是在和底層文檔打交道,DOM就成爲如今javascript編碼中的重要組成部分。瀏覽器把DOM和javascript單獨實現,使用不一樣的引擎。

天生就慢

DOM和javascript就像兩個島嶼經過收費橋樑鏈接,每次經過都要繳納「過橋費」。
推薦的作法是儘量減小過橋的次數,努力待在ECMAScript島上。

DOM訪問與修改

訪問DOM元素是有代價的——前面的提到的「過橋費」。修改元素則更爲昂貴,由於它會致使瀏覽器從新計算頁面的幾何變化(重排)。最壞的狀況是在循環中訪問或修改元素,尤爲是對HTML元素集合循環操做。
在循環訪問頁面元素的內容時,最佳實踐是用局部變量存儲修改中的內容,在循環結束後一次性寫入。
通用的經驗法則是:減小訪問DOM的次數,把運算儘可能留在ECMAScript中處理。

節點克隆

大多數瀏覽器中使用節點克隆都比建立新元素要更有效率。

選擇API

使用css選擇器也是一種定位節點的便利途徑,瀏覽器提供了一個名爲querySelectorAll()的原生DOM方法。這種方法比使用javascript和DOM來遍歷查找元素快不少。使用另外一個便利方法——querySelector()來獲取第一個匹配的節點。

重繪與重排

瀏覽器下載完頁面中的全部組件——HTML標記、javascript、CSS、圖片——以後會解析並生成兩個內部的數據結構:DOM樹(表示頁面結構)、渲染樹(表示DOM節點如何顯示)。當DOM的變化影響了元素的幾何屬性,瀏覽器會使渲染樹中受到影響的部分失效,並重構,這個過程成爲重排,完成後,會從新繪製受影響的部分到屏幕,該過程叫重繪。並非全部的DOM變化都會影響幾何屬性,這時只發生重繪。重繪和重排會致使web應用程序的UI反應遲鈍,應該儘可能避免。

重排什麼時候發生

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

1. 添加或刪除可見的DOM元素
  2. 元素位置改變
  3. 元素尺寸改變(包括:外邊據、內邊距、邊框厚度、寬度、高度等屬性改變)
  4. 內容改變,例如:文本改變或圖片被另外一個不一樣尺寸的圖片代替
  5. 頁面渲染器初始化
  6. 瀏覽器窗口尺寸改變
渲染樹變化的排隊與刷新

因爲每次重排都會產生計算消耗,大多數瀏覽器經過隊列化修改並批量執行來優化重排過程。可是有些操做會致使強制刷新隊列並要求任務馬上執行:

1. offsetTop,offsetLeft,offsetWidth,offsetHeight
  2. scrollTop,scrollLeft,scrollWidth,scrollHeight
  3. clientTop,clientLeft,clientWidth,clientHeight
  4. getComputedStyle()

以上屬性和方法須要返回最新的佈局信息,所以瀏覽器不得不執行渲染隊列中的修改變化並觸發重排以返回正確的值。
最佳實踐:儘可能將修改語句放在一塊兒,查詢語句放在一塊兒。

最小化重繪和重排

爲了減小發生次數,應該合併屢次DOM的樣式的修改,而後一次處理掉。

批量修改DOM

當你須要對DOM元素進行一系列操做時,能夠經過如下步驟來減小重繪和重排的次數:

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

該過程會觸發兩次重排——第一步和第三步,若是忽略這兩步,在第二步所產生的任何修改都會觸發一次重排。

有三種基本的方法可使DOM脫離文檔:

  1. 隱藏元素,應用修改,從新顯示
  2. 使用文檔片斷,在當前DOM以外構建一個子樹,再把它拷貝迴文檔
  3. 將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成後再替換原始元素

推薦使用文檔片斷,由於它們所產生的DOM遍歷和重排次數最少。

緩存緩存佈局信息

當你查詢佈局信息時,瀏覽器爲了返回最新值,會刷新隊列並應用全部變動。
最佳實踐:儘可能減小布局信息的獲取次數,獲取後把它賦值給局部變量,而後操做局部變量。

讓元素脫離動畫流

用展開、摺疊的方式來顯示和隱藏部分頁面是一種常見的交互模式。一般包括展開區域的幾何動畫,並將頁面其餘部分推向下方。通常來講,重排隻影響渲染樹中的一小部分,但也可能影響很大的部分,甚至整個渲染樹。瀏覽器所須要重排的次數越少,應用程序的響應速度就越快。當一個動畫改變整個頁面的餘下部分時,會致使大規模重排。節點越多狀況越差。避免大規模的重排:

1. 使用絕對定位頁面上的動畫元素,將其脫離文檔流。
  2. 應用動畫
  3. 當動畫結束時回恢復定位,從而只會下移一次文檔的其餘元素。

這樣只形成了頁面的一個小區域的重繪,不會產生重排並重繪頁面的大部份內容。

:hover

若是有大量元素使用了:hover,那麼會下降響應速度。此問題在IE8中更爲明顯。

事件委託

當頁面中存在大量元素,而且每個都要一次或屢次綁定事件處理器時,這種狀況可能會影響性能,每綁定一個事件處理器都是有代價的,它要麼加劇了頁面負擔(更多的代碼、標籤),要麼增長了運行期的執行時間。須要訪問和修改的DOM元素越多,應用程序就越慢,特別是事件綁定一般發生在onload時,此時對每個富交互應用的網頁來講都是一個擁堵的時刻。事件綁定佔用了處理事件,並且瀏覽器要跟蹤每一個事件處理器,這也會佔用更多的內存。這些事件處理器中的絕大部分均可能不會被觸發。
事件委託原理:事件逐層冒泡並能被父級元素捕獲。使用事件代理,只須要給外層元素綁定一個處理器,就能夠處理在其子元素上觸發的全部事件。
根據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"元素添加一個點擊監聽器,它會捕獲並分析點擊是否來自連接。

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. 阻止默認動做(可選)

算法和流程控制

循環

循環的類型

ECMA-262標準第三版定義了javascript的基本語法和行爲,其中共有四種循環。

1. 第一種是標準的for循環。它由四部分組成:初始化、前測條件、後執行體、循環體。
           for (var i=0;i<10;i++){
                //do something
           }

for循環是javascript最經常使用的循環結構,直觀的代碼封裝風格被開發者喜好。

2. while循環。while循環是最簡單的前測循環,由一個前測條件和一個循環體構成。

  3. do-while循環是javascript惟一一種後測循環,由一個循環體和一個後測條件組成,至少會執行一次。
  4. for-in循環。能夠枚舉任何對象的屬性名。
循環的性能

javascript提供的四種循環類型中,只有for-in循環比其餘幾種明顯要慢。由於每次迭代操做會同時搜索實例或原型屬性,for-in循環的每次迭代都會產生更多開銷。速度只有其餘類型循環的七分之一。除非你明確須要迭代一個屬性數量未知的對象,不然應該避免使用for-in循環。若是你須要遍歷一個數量有限的已知屬性列表,使用其餘循環類型會更快,好比數組。
除for-in外,其餘循環類型的性能都差很少,類型的選擇應該基於需求而不是性能。

提升循環的性能
1. 減小每次迭代處理的事務
  2. 減小迭代的次數
減小迭代的工做量

減小對象成員及數組項的查找次數。

在不影響的結果的狀況下,可使用倒序來略微提高性能。由於控制條件只要簡單的與零比較。控制條件與true比較時,任何非零數會自動轉換爲true,而零值等同於false,實際上從兩次比較(迭代數少於總數麼?是否爲true?)減小到一次比較(它是true麼)。當循環複雜度爲O(n)時,減小每次迭代的工做量是最有效的方法。當複雜度大於O(n)時,建議着重減小迭代次數。
減小迭代次數

Duff's Device是一個循環體展開技術,使得一次迭代中實際上執行了屢次迭代的操做。一個典型的實現以下:

//credit:Jeff Greenberg
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);

Duff's Device背後的基本理念是:每次循環中最多能夠調用8此process()。循環的迭代次數除以8。因爲不是全部數字都能被8整除,變量startAt用來存放餘數,表示第一次循環中應該調用多少次process()。
此算法稍快的版本取消了switch語句,並將餘數處理和主循環分開

//credit:Jeff Greenberg
var i = items.length % 8;
while(i){
        process(item[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--]);
}

儘管這種實現方法用兩次循環代替以前的一次循環,但它移除了循環體中的switch語句,速度比原始循環更快。
若是循環迭代的次數小於1000,可能它與常規循環結構相比只有微不足道的性能提高。若是迭代數超過1000,那麼執行效率將明顯提高。例如在500000此迭代中,其運行時間比常規循環減小70%

基於函數的迭代

ECMA-262第四版加入的數組方法:forEach()方法。此方法遍歷一個數組的全部成員,並在每一個成員上執行一個函數。要運行的函數做爲參數傳給forEach(),並在調用時接受三個參數,分別是當前的值、索引以及數組自己。儘管基於函數的迭代提供了一個更爲便利的迭代方法,但它仍比基於循環的迭代要慢一些。對每一個數組項調用外部方法所帶來的開銷是速度慢的主要緣由。

條件語句

if-else對比switch

條件數數量越大,越傾向於使用switch,主要是由於易讀性。事實證實,大多數狀況下switch比if-else運行得要快,但只有條件數量很大時才快得明顯。

優化if-else

最小化到達正確分支前所須要判斷的條件數量。最簡單的優化方法是確保最可能出線的條件放在首位。if-else中的條件語句應該老是按照從最大機率到最小几率的順序排列,以確保運行速度最快。假設均勻分部,可以使用二分法的思想,重寫爲一系列嵌套的if-else語句。

查找表

有些時候優化條件語句的最佳方案是避免使用if-else和switch。可使用數組和普通對象來構建查找表,經過查找表訪問數據比用if-else或switch快不少。當單個鍵值存在邏輯映射時,構建查找表的優點就能體現出來。(好比把按照順序的鍵值映射放到數組裏)

遞歸

使用遞歸能夠把複雜的算法變的簡單。潛在問題是終止條件不明確或缺乏終止條件會致使函數長時間運行,並使得用戶界面處於假死狀態和瀏覽器的調用棧大小限制。

調用棧限制

javascript引擎支持的遞歸數量與javascript調用棧大小直接相關。

遞歸模式

當你遇到調用棧大小限制時,第一步應該檢查代碼中的遞歸實例。有兩種遞歸模式,第一種是調用自身,很容易定位錯誤。第二種是互相調用,很難定位。

迭代

任何遞歸能實現的算法一樣可使用迭代來實現。使用優化後的循環代替長時間運行的遞歸函數能夠提高性能,由於運行一個循環比反覆調用一個函數的開銷要少的多。
歸併排序算法是最多見的用遞歸實現的算法:

function merge(left, right) {
    var result = [];

    while (left.length > 0 && right.length > 0){
        if (left[0] < right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());    
        }
    }

    return result.concat(left).concat(right);
}

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
        return merge(mergeSort(left),mergeSort(right));
}

使用迭代實現歸併算法:

//使用和上面相同的merge函數

function mergeSort(items){
    if (items.length == 1){
        return items;
    }

    var work = [];
    for (var i=0, len=items.length;i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);

    for (var lim=len; lim>1; lim = (lim+1)/2){
        for (var j=0,k=0; k < lim; j++, k+=2){
            work[j] = merge(work[k],work[k+1]);
        }
        work[j] = [];
    }

    return work[0];
}

儘管迭代版本的歸併排序算法比遞歸實現得要慢一些,但它不會像遞歸版本那樣受到調用棧限制的影響。把遞歸算法改用迭代實現是避免棧溢出錯誤的方法之一

Memoization

Memoization是一種避免重複工做的方法,它緩存前一個計算結果供後續計算使用,避免了重複工做。
使用Memoization技術來重寫階乘函數:

function memfactorial(n){
    if(!memfactorial.cache){
        memfactorial.cache={
            "0":1,
            "1":1
        };
    }

    if(!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial (n-1);
    }

    return memfactorial.cache[n];
}

字符串和正則表達式

字符串連接

+和+=

不該在等號右邊進行和被賦值的量無關的字符串拼接運算,這樣會創造臨時字符串。
例如:

str += "one" + "two";

會經歷四個步驟:

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

使用這種方式來代替:

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

賦值表達式由str開始做爲基礎,每次給它附加一個字符串,由作到右一次鏈接,所以避免了使用臨時字符串。

數組項合併

Array.prototype.join方法將數組的全部元素合併成一個字符串,它接受一個字符串參數做爲分隔符插入每一個元素的中間。大多數瀏覽器中,數組項合併比其餘字符串鏈接的方法更慢。

String.prototype.concat

字符串的原生方法concat能接收任意數量的參數,並將每個參數附加到所調用的字符串上。這是最靈活的字符串合併方法。多數狀況下,使用concat比使用簡單的+和+=稍慢。

正則表達式優化

部分匹配比徹底不匹配所用的時間要長。

正則表達式工做原理
1.  第一步編譯

瀏覽器會驗證正則表達式,而後把它轉換爲一個原生代碼程序,用於執行匹配工 做。若是把正則對象賦值給一個變量,能夠避免重複這一步。

2.  第二步設置起始位置
  3.  第三步匹配每一個正則表達式字元
  4.  第四步匹配成功或失敗
回溯

當正則比到達時匹配目標字符串時,從左到右逐個測試表達式的組成部分,看是否能找到匹配項。在遇到量詞和分支時,須要決策下一步如何處理。若是遇到量詞,正則表達式需決定什麼時候嘗試匹配更多字符;若是遇到分支,那麼必須從可選項中選擇一個嘗試匹配。每當正則表達式作相似的決定時,若是有必要的話,都會記錄其餘選擇,以備返回時使用。若是當前選項匹配成功,正則表達式繼續掃描表達式,若是其餘部分也匹配成功,尼瑪匹配結束。可是若是當前選項找不到匹配值,或後面的部分匹配失敗,那麼正則表達式會回溯到最後一個決策點,而後在剩餘的選項中選擇一個。這個過程會一直進行,知道找到匹配項,或者正則表達式中量詞和分支選項的全部排列組合都嘗試失敗,那麼它將放棄匹配從而移動到字符串的下一個字符,再重複此過程。

重複和回溯

貪婪匹配是段尾一個個回溯接下來的匹配內容,惰性正好相反;

回調失控

最佳實踐:若是你的正則表達式包含了多個捕獲組,那麼你須要使用適當的反向引用次數。

嵌套量詞與回溯失控

所謂的嵌套量詞須要格外的關注且當心使用,以確保不會引起潛在的回溯失控。嵌套兩次是指兩次出線在一個自身被重複量詞修飾的組中。確保正則表達式的兩個部分不能對字符串的相同部分進行匹配

更多提升正則表達式效率的方法
1.  關於如何讓正則匹配更快失敗

正則表達式慢的緣由一般是匹配失敗的過程慢。

2.  正則表達式以簡單、必需的字元開始

一個正則表達式的起始標記應當儘量快速的測試並排除明顯不匹配的位置。儘可能以一個錨、特定字符串、字符類和單詞邊界開始,儘可能避免以分組或選擇字元開頭,避免頂層分支。

3.  使用量詞模式,使它們後面的字元互斥

當字符與字元相鄰或子表達式可以重疊匹配時,正則表達式嘗試拆解文本的路徑數量將增長。

4.  減小分支數量,縮小分支範圍

分支使用豎線|可能要求在字符串的每個位置上測試全部的分支選項。你一般能夠經過使用字符集和選項組件來減小對分支的需求,或將分支在正則表達式上的位置推後。

5.  使用非捕獲組

捕獲組消耗時間和內存來記錄反向引用,並使它保持最新。若是你不須要一個反向引用,可使用非捕獲組來避免這些開銷。

6.  只捕獲感興趣的文本以減小後處理

若是須要引用匹配的一部分,應該纔去一切手段捕獲那些片斷,再使用反向引用來處理。

7.  暴露必需的字元

嘗試讓正則表達式引擎更容易判斷哪些字元是必需的。

8.  使用合適的量詞
9.  把正則表達式賦值給變量並重用它們

避免在循環體中重複編譯正則表達式。

10.  將複雜的正則表達式拆分爲簡單的片斷

什麼時候不使用正則表達式

當只是搜索字面字符串,尤爲是事先知道字符串的哪一部分將要被查找時。正則表達式沒法直接跳到字符串末尾而不考慮沿途的字符。


快速響應的用戶界面

瀏覽器UI線程

用於執行Javascript和更新用戶界面的進程一般被稱爲「瀏覽器UI線程」。UI線程的工做基於一個見到那的隊列系統,任務會被保存到隊列中直到線程空閒。

瀏覽器限制

瀏覽器限制了javascript的運行時間。此類限制分爲兩種:調用棧的大小限制和長時間運行腳本限制。

多久算過久

單個Javascript操做話費的總時間不該該超過100毫秒。
最佳實踐:限制全部的Javascript任務在100毫秒或更短的時間內完成。

使用定時器讓出時間片斷

當Javascript不能在100毫秒或更短的時間內完成。最理想的方法是讓出UI線程的控制權,使得UI能夠更新。

定時器基礎

在Javascript中可使用setTimeout()和setInterval()建立定時器,它們接收相同的參數:要執行的函數和執行前的等待時間。定時器與UI線程的交互:定時器會告訴Javascript引擎先等待必定時間,而後添加一個Javascript任務到UI隊列。定時器代碼只有在建立它的函數執行完以後,纔有可能執行。不管發生何種狀況,建立一個定時器會形成UI線程暫停,如同它從一個任務切換到下一個任務。所以,定時器代碼會重置全部相關的瀏覽器限制,包括 長時間運行腳本定時器。此外,調用棧也會在定時器中重置爲0。setTimeout()和setInterval()幾近相同,若是 UI隊列中已經存在由同一個setInterval()建立的任務,那麼後續任務不會被添加到UI隊列中。若是setTimeout()中的函數須要消耗比定時器延時更長的運行時間,那麼定時器代碼中的延時幾乎是不可見的。

定時器的精度

Javascript定時器延遲一般不太準確,相差大約爲幾毫秒,沒法用來精確計算時間。並且還存在最小值的限制。

使用定時器處理數組

是否能夠用定時器取代循環的兩個決定性因素:處理過程是否必須同步;數據是否必須按照順序處理;若是兩個答案都是否,那麼代碼適用於定時器分解任務。

var todo = items.concat();
// 克隆原數組

setTimeout(function(){

    // 取得數組的下一個元素並進行處理
    process(todo.shift());

    // 若是還有須要處理的元素,建立另外一個定時器
    if(todo.length > 0){
        setTimeout(arguments.callee, 25);
    } else {
        callback(items);
    }

}, 25);

每一個定時器的真實延時在很程度上取決於具體狀況。廣泛來說,最好使用至少25毫秒,由於再小的延時,對大多數UI更新來講不夠用。

記錄代碼運行使勁啊

經過定時器建立Date對象並比較它們的值來記錄代碼運行事件。加號能夠將Date對象轉換成數字,那麼在後續的運算中就無須轉換了。避免把任務分解成過於零碎的碎片,由於定時器之間有最小間隔,會致使出線空閒事件。

定時器與性能

當多個重複的定時器同時建立每每會出線性能問題。由於只有一個UI線程,而全部的定時器都在爭奪運行時間。那些間隔在1秒或1秒以上的低頻率的重複定時器幾乎不會影響Web應用的響應速度。這種狀況下定時器延遲遠遠超過UI線程產生瓶頸的值,能夠安全的重複使用。當過個定時器使用較高的頻率(100到200毫秒之間)時,會明顯影響性能。在web應用中限制高頻率重複定時器的數量,做爲代替方案,使用一個獨立的重複定時器每次執行多個操做。

Web Worker

引入了一個接口,能使代碼運行而且不佔用瀏覽器UI線程的時間。

Worker

沒有綁定UI線程,每一個Web Worker都有本身的全局環境,其功能只是Javascript特性的一個子集。運行環境由以下部分組成:一個navigator對象,值包括四個屬性:appName、appVersion、userAgent和platform。
一個location對象(與window.location相同,不過全部屬性都是隻讀的。)。
一個self對象,指向全局worker對象。
一個importScipt()方法,用來加載Worker所用到的外部javascript文件。
全部的ECMAScript對象
XMLHttpRequest構造器
setTimeout()方法和setInterval()方法
一個close()方法,它能馬上中止Worker運行
因爲Web Worker有着不一樣的全局運行環境,所以你沒法從javascript代碼中建立它。須要建立一個徹底獨立的javascript文件,其中包含了須要在Worker中運行的代碼。要建立網頁人工線程,你必須傳入這個javascript文件的URL;

與Worker通訊

經過事件接口進行通訊。網頁代碼能夠經過postMessage()方法給Worker傳遞數據,它接受一個參數,即須要傳遞給Worker的數據。此外,Worker還有一個用來接收信息的onmessage事件處理器。Worker可經過它本身的postMessage()方法把信息回傳給頁面。消息系統是網頁和Worker通訊的惟一途徑。只有特定類型的數據可使用postMessage()傳遞。你能夠傳遞原始值(字符串、數字、布爾值、null和undefined),也能夠傳遞Object和Array的實例,其餘類型就不容許了。有效數據會被序列化,傳入或傳出Worker,而後反序列化。雖然看上去對象能夠直接傳入,但對象實例徹底是相同數據的獨立表述。

加載外部文件

Worker 經過importScript()方法加載外部javascript文件,該方法接收一個或多個javascript文件URL做爲參數。importScript()的調用過程是阻塞式的,知道全部全部文件加載並執行完成以後,腳本纔會繼續運行。因爲Worker在UI線程以外運行,因此這種阻塞並不會影響UI響應。
Web Worker適合用於那些處理純數據,或者與瀏覽器UI無關的長時間運行腳本。儘管它看上去用處不大,但Web應用中一般有一些數據處理功能將收益於Worker而不是定時器。
可能的用處:

  1. 編碼/解碼大字符串
  2. 複雜數學運算
  3. 大數組排序
  4. 任何超過100毫秒的處理過程,都應當考慮Worker方案是否是比基於定時器的方案更爲合適。

Ajax

Ajax是高性能javascript的基礎。它能夠經過延遲下載體積較大的資源文件來使得頁面加載速度更快。它經過異步的方式在客戶端和服務端之間傳輸數據,避免同時傳輸大量數據。

數據傳輸

請求數據

有五種經常使用技術用於想服務器請求數據:

  1. XMLHttpRequest
  2. Dynamic script tag insertion(腳本動態注入)
  3. iframes
  4. Comet
  5. Multipart XHR

現代高性能Javascript中使用的三種技術是:XHR、動態腳本注入和Multipart XHR

XMLHttpRequest

XMLHttpRequest是目前最經常使用的技術,它容許異步發送和接收數據。因爲XHR提供了高級的控制,因此瀏覽器對其增長了一些限制。你不能使用XHR從外域請求數據。對於那些不會改變服務器狀態,只會獲取數據(冪等行爲)的請求,應該使用GET。經GET請求的數據會被緩存起來,若是須要屢次請求統一數據的話,它會有助於提高性能。只有當請求的URL加上參數的長度接近或超過2048個字符時,才應該用POST獲取數據。由於IE限制URL長度,過長將致使請求的URL被截斷。

動態腳本注入

這種技術客服了XHR的最大限制:它能跨域請求數據。這是一個Hack,你不須要實例化一個專用對象,而可使用javascript建立一個新的腳本標籤,並設置它的src屬性爲不一樣域的URL。與XHR相比,動態腳本注入提供的控制是有限的。只能使用GET方法而不是POST方法。不能設置請求的超時處理或重試;不能訪問請求的頭部信息,不能把整個響應信息做爲字符串來處理。由於響應消息做爲腳本標籤的源碼,它必須是可執行的javascript代碼。你不能使用純XML、純JSOn或其餘任何格式的數據,不管哪一種格式,都必須封裝在一個回調函數中。這項技術的速度卻很是快。響應消息是做爲javascript執行,而不是做爲字符串須要進一步處理。正因如此,它有潛力成爲客戶端獲取並解析數據最快的方法。

Multipart XHR

容許客戶端只用一個HTTP請求就能夠從服務端向客戶端傳送多個字元。它經過在服務端將字元打包成一個由雙方約定的字符串分割的長字符串併發送到客戶端。而後用javascript代碼處理這個長字符串,並根據它的mime-type類型和傳入的其餘「頭信息」解析出每一個資源。缺點:資源不能被瀏覽器緩存。
能顯著提升性能的場景:
頁面包含了大量其餘地方用不到的資源,尤爲是圖片;
網站已經在每一個頁面中使用了一個獨立打包的Javascript或CSS文件以減小http請求;

發送數據

XMLHttpRequest
當使用XHR發送數據到服務器時,GET方式會更快。這是由於,對少許數據而言一個GET請求只發送一個數據包。而一個POST請求至少要發兩個數據包,一個裝載頭信息,另外一個裝載POST正文。POST更適合發送大量數據到服務器,由於它不關心額外數據包的數量,另外一個緣由是URL長度有限制,它不可能使用過長的GET請求。

Beacons

相似動態腳本注入。使用Javascript建立一個新的Image對象,並把src屬性設置爲服務器上腳本的URL。該URL包含了咱們要經過GET傳回的鍵值對數據。服務器會接受數據並保存下來,無須向客服端發送任何回饋信息,所以沒有圖片會實際顯示出來。這是回傳信息最有效的方式。性能消耗更小,並且服務器端的錯誤不影響客戶端。缺點:沒法發送POST數據,而URL的長度有最大值,因此能夠發送的數據的長度被限制的至關小。

數據格式

考慮數據格式時惟一須要比較的標準就是速度

XML

當Ajax最早開始流行時,它選擇了XML做爲數據格式。優點:極佳的通用性、格式嚴格,且易於驗證。缺點:冗長,依賴大量結構、有效數據的比例很低、語法模糊,若是有其餘格式可選不要使用它。

JSON

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

JSON-P

事實上,JSON能夠被本地執行會致使幾個重要的性能影響。當使用XHR時,JSON數據被當成字符串返回。在使用動態腳本注入時,JSON數據要被當成另外一個Javascript文件並做爲原生代碼執行,爲實現這一點必須封裝在一個回調函數中。JSON-P由於回調包裝的緣由略微增大了文件尺寸,但性能提高巨大。因爲數據是看成原生的Javascript,所以解析速度跟原生Javascript同樣快。最快的JSON格式是使用數組形式的JSON-P。不要把敏感數據編碼在JSON-P中,由於沒法確認它是否保持着私有調用狀態。

HTML

一般你請求的數據須要被轉換成HTML以顯示到頁面上。Javascript能夠較快地把一個較大的數據結構轉換成簡單的HTML,但在服務器處理會快不少。一種可考慮的技術是在服務器上構建好整個HTML再傳回客戶端,Javascript能夠很方便地經過innerHTML屬性把它插入頁面相應的位置。取點:臃腫的數據格式、比XML更繁雜。在數據自己的最外層,能夠嵌套HTML標籤,每一個都帶有id、class和其餘屬性。HTML格式可能比實際數據佔用更多空間。應當在客戶端的瓶頸是CPU而不是帶寬時才使用此技術。

自定義格式

理想的數據格式應該只包含必要的結構,以便你能夠分解出每一個獨立的字段。最重要的決定就是採用哪一種分隔符,它應當是一個單字符,並且不該該存在你的數據中。

Ajax性能指南

緩存數據

在服務端,設置HTTP頭信息以確保你的響應會被瀏覽器緩存。
在客戶端,把獲取到的信息存儲到本地,從而避免再次請求。

設置HTTP頭信息

若是但願ajax能被瀏覽器緩存,那麼你必須使用GET方式發送請求而且須要在響應中發送正確的HTTP頭信息。Expires頭信息會告訴瀏覽器應該緩存多久。它的值是一個日期。

本地數據存儲

直接把從服務器接收到的數據儲存起來。能夠把響應文本保存到一個對象中,以URL爲鍵值做爲索引。

Ajax類庫的侷限性

全部的Javascript類庫都容許你訪問一個Ajax對象,它屏蔽了瀏覽器之間的差別,給你一個統一的接口。爲了統一接口的功能,類庫簡化接口,使得你不能訪問XMLHttpRequest的完整功能。


編程實踐

避免雙重求值

Javascript容許你在程序中提取一個包含代碼的字符串,而後動態執行它。有四種標準方法能夠實現:eval()、Function()構造函數、setTimeout()和setInterval()。首先會以正常的方式求值,而後在執行的過程當中對包含於字符串的代碼發起另外一個求值運算。每次使用這些方法都要建立一個新的解釋器/編譯器實例,致使消耗時間大大增長。
大多數時候沒有必要使用eval()和Function(),所以最好避免使用它們。定時器則建議傳入函數而不是字符串做爲第一個參數。

使用Object/Array直接量

Javascript中建立對象和數組的方法有多種,但使用對象和數組直接量是最快的方式。

避免重複工做

別作可有可無的工做,別重複作已經完成的工做。

延遲加載

第一次被調用時,會先檢查並決定使用哪一種方法去綁定或取消綁定事件處理器。而後原始函數被包含正確操做的新函數覆蓋。最後一步調用新的函數,並傳入原始參數。隨後每次調用都不會再作檢測,由於檢測代碼已經被新的函數覆蓋。調用延遲加載函數時,第一次總會消耗較長的費時間,由於它必須運行檢測接着再調用另外一個函數完成任務。但隨後調用相同的函數會更快,由於不須要再執行檢測邏輯。當一個函數在頁面中不會馬上調用時,延遲加載是最好的選擇。

條件預加載

它會在腳本加載期間提早檢測,而不會等到函數被調用。檢測的操做依然只有一次,知識它在過程當中來的更早。條件預加載確保全部函數調用消耗的時間相同。其代價是須要在腳本加載時就檢測,而不是加載後。預加載適用於一個函數立刻就要被用到,而且在整個頁面的生命週期中頻繁出現的場合。

使用快的部分

運行速度慢的部分其實是代碼,引擎一般是處理過程當中最快的部分。

位操做

使用位運算代替純數學操做:對2的取模運算能夠被&1代替,速度提升不少。位掩碼:處理同時存在多個布爾選項時的情形,思路是使用單個數字的每一位來斷定是否選項成立,從而有效得把數字轉換爲布爾值標記組成的數組。

原生方法

原生方法更快,由於寫代碼前就存在瀏覽器中了,而且都是用底層語言好比c++編寫的。經驗不足的Javascript開發者常常犯的錯誤就是在代碼中進行復雜的數學運算,而沒有使用內置的Math對象中那些性能更好的版本。另外一個例子是選擇器API,它容許使用CSS選擇器來查找DOM節點。原生的querySelector()和querySelectorAll()方法完成任務平均所需時間是基於Javascript的CSS查詢的10%。


構建並部署高性能Javascript應用

合併多個Javascript文件,網站提速指南中第一條也是最重要的一條規則,就是減小http請求數。

預處理Javascript文件

預處理你的Javascript源文件並不會讓應用變的更快,但它容許你作些其餘的事情,例若有條件地插入測試代碼,來衡量你的應用程序的性能。

Javascript壓縮

指的是把Javascript文件中全部與運行無關的部分進行剝離的過程。剝離的內容包括註釋和沒必要要的空白字符。該過程一般能夠將文件大小減半,促使文件更快被下載,並鼓勵程序員編寫更好的更詳細的註釋。

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

廣泛規則是隻要能在構建時完成的工做,就不要留到運行時去作。

Javascript的http壓縮

當Web瀏覽器請求一個資源時,它一般會發送一個Accept-Encoding HTTP頭來告訴Web服務器它支持哪一種編碼轉換類型。這個信息主要用來壓縮文檔以更快的下載,從而改善用戶體驗。Accept-Encoding可用的值包括:gzip、compress、deflate和identity。gzip是目前最流行的編碼方式。它一般能減小70%的下載量,成爲提高Web應用性能的首選武器。記住Gzip壓縮主要適用於文本,包括Javascript文件。

緩存Javascript文件

緩存HTTP組件能極大提升網站回訪用戶的體驗。Web服務器經過Expires HTTP響應頭來告訴客戶端一個字元應當緩存多長事件。它的值是一個遵循RFC1123標準的絕對時間戳。

處理緩存問題

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

使用內容分發網絡(CDN)

內容分發網絡是在互聯網上按地理位置設置分部計算機網絡,它負責傳遞內容給終端用戶。使用CDN的主要緣由是加強Web應用的可靠性、可擴展性,更重要的是提高性能。事實上,經過向地理位置最近的用戶輸入內容,CDN能極大減小網絡延時。

相關文章
相關標籤/搜索