1. 程式人生 > >高性能JavaScript(您值得一看)

高性能JavaScript(您值得一看)

銷毀 使用方法 三次 應用 布爾 表達式 步驟 深度 還需要

閱讀目錄

  • Javascript第一條定律:將腳本放在底部。
  • Javascript第二條定律:將腳本成組打包。
  • 雜談
  • 盡量使用局部變量來保存全局變量
  • 盡量少去改變作用域鏈
  • 盡量少去使用閉包
  • 訪問速度與成員嵌套深度有關
  • 緩存變量的值
  • 1、在修改DOM元素的時候,我們應該盡量使用innerHTML而不是CreateElement再AppendChild(因為經過測試,在所有的瀏覽器當中使用innerHTML更加快一些)
  • 2、修改DOM元素內容時另外一個方法是使用element.cloneNode來代替document.createElement()
  • 3、在循環或者遍歷元素時,盡量使用局部變量保存HTML Collections
  • 4、childNodes是一個集合,這也是為什麽很多提高JavaScript性能的建議當中有這樣一條了“使用firstChild和nextSibling代替childNodes遍歷dom元素” 用圖來說話吧(不信你可以測試一下,明顯testNextSibling的速度比testChildNodes的快特別是當數據量大的時候一眼就能看出來)
  • 5、如果瀏覽器使用具有原生的QuerySelectorAll()方法和querySelector()方法盡量使用它們(因為自己實現它們的功能需要編寫特別多的代碼,還有就是任何編程語言都有一個共通的特性就是原生方法永遠是性能最佳的)
  • 6、最小化重繪和重排

眾所周知瀏覽器是使用單進程處理UI更新和JavaScript運行等多個任務的,而同一時間只能有一個任務被執行,如此說來,JavaScript運行了多長時間就意味著用戶得等待瀏覽器響應需要花多久時間。

從認知上來說,解析器解析一個界面的時候都是從上至下依次解析的,這就是說界面上出現多少個<script>標簽(不管是內聯還是外部文件),頁面下載和解析必須停止等待腳本下載完成並運行完成(註意這裏包括運行),這個過程當中,頁面解析和用戶交互是被完全阻塞的。

回到頂部

Javascript第一條定律:將腳本放在底部。

大家都知道,瀏覽器在遇到<body>標簽之前,不會渲染頁面中的任何部分,如果我們把<script>標簽全部放在<body>標簽之前,大家想想界面是不是一片空白,直至瀏覽器將所有js都下載並執行後才會去渲染頁面,這也就是為什麽要將所有<script>標簽放在盡可能接近<body>標簽底部也就是</body>前的原因。

回到頂部

Javascript第二條定律:將腳本成組打包。

減少引用外部腳本文件的數量,大家相信都會看到一些大型網站需要多次請求JavaScript文件時會將這個文件整合成一個文件,因為這樣只需要一個<script>標簽而減少性能損失。

書中介紹了運行一個<script>標簽來加載兩個js文件(但是自己沒有試驗成功,如果有知道怎麽用的,可以告知我,謝謝!)

技術分享

在此特意說明一下

  • 為什麽很多人特別喜歡用外部文件來寫js而不用內聯js,這是因為1)外部文件易於管理2)瀏覽器有緩存外部文件的功能,同樣一個js文件如果被多個界面共享使用,第二個使用的可以直接從緩存中拿而不用從遠程服務器上去下載,省去了好多時間及流量呀
  • 為什麽許多人會用CDN(Content Delivery Network)來加載公用js比如說jquery.js,這是因為從別的網站比如說jQuery官網下載jQuery占有的是別人服務器的資源,減輕了本地服務器的負載,節省網站分布式架構的支出成本和運維成本。
  • 上文中講了將多個js文件打包成一個js文件,可以提高性能,可以試試用Yahoo! Combo handler ,這篇文章介紹了一下http://www.ooso.net/archives/458 不過百度一下js打包工具一大堆,大家可以任意發揮哦

總歸一句話就是加載js的時候會阻塞頁面渲染,那怎麽樣讓腳本不阻塞頁面渲染呢,唯一的方法就是在window.onload發生後開始下載js

Javascript第三條定律:使用非阻塞方式下載js。

第一種方法:為script增加了一個defer屬性,defer屬性指定的script指明腳本不打算修改DOM,所以代碼可以稍後執行。但是defer並不是所有瀏覽器都支持的。當defer指定的腳本文件被下載時,它不會阻塞瀏覽器的其它處理過程,所以事實上defer指定的腳本文件是可以與頁面中的其它資源並行下載的。任何被指定為defer的腳本文件一定會在DOM加載完成之後才會被執行,也就是在onload事件之前被執行。

第二種方法:動態創建腳本也就是通過js創建script標簽指定它的src來加載腳本文件,這種方法使得腳本下載和運行不會阻塞其它頁面處理程序,所以你甚至可以把這種動態創建腳本的文件放在head標簽當中都沒有關系。

動態創建腳本的js如下圖所示:

技術分享

上述動態創建的腳本只要一指定它的src並append到document中,它就會自動執行裏面的代碼,那怎麽樣讓它加載完成之後執行回調函數或者說如果我要加載多個js文件合格保證它們的順序呢

看代碼吧! 哈哈(因為動態加載的js,瀏覽器不保證加載的順序)

技術分享

另外一種以非阻塞方式獲得腳本文件的方法就是使用XHR(XmlHttpRequest腳本註入)

技術分享

這種方法的主要優點是你可以下載不立即執行的腳本文件,但是缺點就是你要動態加載的js文件必須與當前運行的頁面在同一個域中

下面介紹幾個動態加載的腳本庫

LazyLoad--可以下載多個腳本文件並保證在所有瀏覽器上保證順序執行。(雖然可以動態加載多個文件,但還是建議盡可能的減少文件數量,因為每個下載仍然是一個單獨的HTTP請求)

技術分享

Labs.js 它的script方法用於加載js文件,wait方法用於等待文件下載並運行之後執行的回調函數,通過wait方法允許你哪些文件應該等待其它文件,具體用法可以參照labs使用方法

技術分享

回到頂部

雜談

對於任何一種編程語言來說,數據存儲的位置關系到訪問速度!

在JavaScript中的直接量包括字符串string、數字number、布爾值boolean、對象object、數組array、函數function、正則表達式regular expression、空值null、未定義數組undefined。而數組項則需要通過數組的數字索引來訪問,對象通過字符串進行索引來訪問其成員(這裏順便提一句因為數組項是通過數字進行索引、對象成員是通過字符串進行索引,所以這也就是為什麽訪問對象成員比訪問數組項更慢的原因)。

總結一句話就是說直接量和局部變量的訪問速度要遠已於數組項和對象成員,所以我們應該盡量使用直接量和局部變量,盡量限制使用數組項和對象成員。

了解過JavaScript原型鏈和閉包的都知道在JavaScript中每個函數事實上就是對象,每個函數內部都會有一個Scope屬性,這個屬性包含被創建的作用域中對象的集合,所以事實上內部Scope屬性就是函數的作用域。在函數運行時,會建立一個運行期上下文(運行期上下文事實上就是函數運行時的環境,對於函數的每次運行而言,運行期上下文都是獨一的,函數執行完畢運行期上下文將被銷毀,也就是說多次運行同一個函數就會創建多個運行期上下文)。

當函數運行期上下文確定後,它的作用域將被初始化(作用域初始化包括函數參數、函數內部變量、this),作用域初始化之後將被作為激活對象推入作用域鏈的前端。(事實上這就是原型鏈的原理,js就是通過原型鏈來實現繼承,通過閉包來實現成員的公有和私有),這也是為什麽訪問全局變量是最慢的,因為全局變量它是處於運行期上下文作用域鏈的最後一個位置。

回到頂部

盡量使用局部變量來保存全局變量

如果需要多次訪問全局變量,盡量使用局部變量來保存它(我覺得這條規則運用在哪種編程語言中都適用)

技術分享

回到頂部

盡量少去改變作用域鏈

  • 使用with
  • try catch

我了解到的JavaScript中改變作用域鏈的方式只有兩種1)使用with表達式 2)通過捕獲異常try catch來實現

但是with是大家都深惡痛絕的影響性能的表達式,因為我們完全可以通過使用一個局部變量的方式來取代它(因為with的原理是它的改變作用域鏈的同時需要保存很多信息以保證完成當前操作後恢復之前的作用域鏈,這些就明顯的影響到了性能)

try catch中的catch子句同樣可以改變作用域鏈。當try塊發生錯誤時,程序自動轉入catch塊並將異常對象推入作用域鏈前端的一個可變對象中,也就是說在catch塊中,函數所有的局部變量已經被放在第二個作用域鏈對象中,但是catch子句執行完成之後,作用域鏈就會返回到原來的狀態。應該最小化catch子句來保證代碼性能,如果知道錯誤的概念很高,我們應該盡量修正錯誤而不是使用try catch

回到頂部

盡量少去使用閉包

眾所周知,閉包最厲害的地方就是允許函數使用局部範圍之外的數據。

大家知道當函數運行完成之後,其運行期上下文就將被銷毀,當涉及閉包的時候,它就不能被銷毀,因為可能它還需要被函數引用,這樣無形中就需要更多的內存開銷。

回到頂部

訪問速度與成員嵌套深度有關

成員嵌套越深訪問速度越慢

location.href快於window.location.href

window.location.href快於window.location.href.toString()

回到頂部

緩存變量的值

(事實上就是如果需要訪問全局變量或者多次訪問具有深度的對象成員,使用局部變量將其保存下來以方便後續使用)

在JavaScript高級程序設計第一章當中就把JavaScript分成三大部分

技術分享

所以事實上DOM和BOM是兩在獨立的部分,它們之間的通信是通過相互之間的功能接口來實現的,這樣說來兩個獨立的部分以功能接口必定會帶來性能損耗。這也就是為什麽大家一致都說盡量少去訪問和修改DOM元素(註意我這裏說的是訪問和修改,為什麽包括訪問,請繼續往下看 哈哈)。

下面用一張圖來說明它們各自的作用。

技術分享

回到頂部

1、在修改DOM元素的時候,我們應該盡量使用innerHTML而不是CreateElement再AppendChild(因為經過測試,在所有的瀏覽器當中使用innerHTML更加快一些)

回到頂部

2、修改DOM元素內容時另外一個方法是使用element.cloneNode來代替document.createElement()

回到頂部

3、在循環或者遍歷元素時,盡量使用局部變量保存HTML Collections

那具體什麽叫HTML Collections,它包括哪些元素呢?

技術分享

總結一句話就是說HTML Collections事實上是虛擬存在的,意味著當底層文檔更新時,它們將自動更新。當每次叠代訪問HTML Collections的length屬性時,它會導致集合器更新,所以將length屬性緩存到一個變量中,然後在循環判斷條件中使用這個變量。

技術分享

回到頂部

4、childNodes是一個集合,這也是為什麽很多提高JavaScript性能的建議當中有這樣一條了“使用firstChild和nextSibling代替childNodes遍歷dom元素” 用圖來說話吧(不信你可以測試一下,明顯testNextSibling的速度比testChildNodes的快特別是當數據量大的時候一眼就能看出來)

技術分享

回到頂部

5、如果瀏覽器使用具有原生的QuerySelectorAll()方法和querySelector()方法盡量使用它們(因為自己實現它們的功能需要編寫特別多的代碼,還有就是任何編程語言都有一個共通的特性就是原生方法永遠是性能最佳的)

回到頂部

6、最小化重繪和重排

在說明這個的時候順便理清一下什麽叫重繪,什麽叫重排

大家都知道當一個頁面展示在我們面前的時候,事實上瀏覽器下載完成所有的html標記,js,css,圖片之後,它解析文件並創建兩個內部結構,一個是DOM樹,一個是渲染樹。瀏覽器根據DOM樹來進行排列,根據渲染樹來進行繪制。

什麽情況下會發什麽重排呢

  • 添加或刪除可見的DOM元素
  • 元素位置改變
  • 元素尺寸改變(因為邊距、填充、邊框寬高等屬性)
  • 內容改變
  • 最初的頁面渲染
  • 瀏覽器窗口改變尺寸

理解了重排,重繪就更加簡單了,從字面意思上來說就是重新繪制,那什麽情況下會發生重繪,比如說改變樣式背景等

技術分享

類似的我們知道了這個也應該在腳本中註意盡量減少repaint和reflow,什麽情況會導致這兩種情況呢

reflow:當DOM元素出現隱藏/顯示、尺寸變化、位置變化的時候都會讓瀏覽器重新渲染頁面,之前渲染工作白費了

repaint:當元素的背景顏色、邊框顏色不引起reflow的變化是會讓瀏覽器重新渲染該元素。貌似還可以接受,但如果我們在開始就定義好了,不讓瀏覽器重復工作就更好了。

上圖中列舉的這些屬性的訪問將會導致重新刷新渲染樹從而導致重排,最好減少對這些屬性(布局信息)的查詢次數,查詢時將它賦給局部變量,並用局部變量參與計算。

再來看一個小例子

技術分享

這明顯是糟糕的代碼,改變了三次風格屬性,導致瀏覽器重排了三次。一個提高效率的方法就是只重排一次。看下面代碼就明白了

技術分享

或者使用類名的方法

技術分享

當你需要對DOM樹中的多個元素進行修改的話,最好依據下列步驟來減少重排和重繪

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

有三種方式可以將元素從文檔流中摘除

  • 隱藏元素,對其進行修改後再顯示
  • 使用文檔片斷在已存DOM之外創建一個子樹,然後將它拷貝到文檔中(推薦使用此方法,因為它是涉及最小數量的DOM操作和重排)
  • 將原始元素拷貝到一個脫離文檔的節點中,修改副本,然後副本,然後覆蓋原始元素

將第二次方案的代碼貼一下,希望大家能使用這種方法來提高性能。

技術分享
       for (var i = 0; i < 1000; i++) {
            var el = document.createElement(‘p‘);
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替換為:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement(‘p‘);
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
技術分享

如果是動畫元素的話,最好使用絕對定位以讓它不在文檔流中,這樣的話改變它的位置不會引起頁面其它元素重排

(我要說的就這些啦,希望大家共同探討!)

這是我看高性能JavaScript的一點總結,希望對前端感興趣的可以與我共同研討!

有興趣的可以去了解一下lab.js和lazyload 這倆挺好用的。哈哈!也可以跟我共同研討!

高性能JavaScript(您值得一看)