本文爲 《高性能 JavaScript》 讀書筆記,是利用中午休息時間、下班時間以及週末整理出來的,此書雖有點老舊,但談論的性能優化話題是每位同窗必須理解和掌握的,業務響應速度直接影響用戶體驗。javascript
大多數瀏覽器使用單進程處理UI
更新和JavaScript
運行等多個任務,而同一時間只能有一個任務被執行
將全部script
標籤放在頁面底部,緊靠</body>
上方,以保證頁面腳本運行以前完成解析css
<html> <head> </head> <body> <p>Hello World</p> <!-- --> <script type="text/javascript" src="file.js"></script> </body> </html>
常規script
腳本瀏覽器會當即加載並執行,異步加載使用async
與defer
兩者區別在於aysnc
爲無序,defer
會異步根據腳本位置前後依次加載執行html
<!-- file一、file2依次加載 --> <script type="text/javascript" src="file1.js" defer></script> <script type="text/javascript" src="file2.js" defer></script>
<!-- file一、file2無序加載 --> <script type="text/javascript" src="file1.js" async></script> <script type="text/javascript" src="file2.js" async></script>
不管在何處啓動下載,文件的下載和運行都不會阻塞其餘頁面處理過程。你甚至能夠將這些代碼放在<head>
部分而不會對其他部分的頁面代碼形成影響(除了用於下載文件的 HTTP
鏈接)前端
var script = document.createElement("script"); script.type = "text/javascript"; script.src = "file1.js"; document.getElementsByTagName("head")[0].appendChild(script);
function loadScript(url, callback) { var script = document.createElement("script"); script.type = "text/javascript"; if (script.readyState) { //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function() { callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
前提條件爲同域,此處與異步加載同樣,只不過使用的是 XMLHttpRequestjava
script
標籤放在頁面底部,緊靠 body 關閉標籤上方,以保證頁面腳本運行以前完成解析數據存儲在哪裏,關係到代碼運行期間數據被檢索到的速度.每一種數據存儲位置都具備特定的讀寫操做負擔。大多數狀況下,對一個直接量和一個局部變量數據訪問的性能差別是微不足道的。
對 DOM 操做代價昂貴,在富網頁應用中一般是一個性能瓶頸。一般處理如下三點ajax
經過 DOM 事件處理用戶響應正則表達式
一個很形象的比喻是把 DOM 當作一個島嶼,把 JavaScript(ECMAScript)當作另外一個島嶼,二者之間以一座收費橋鏈接(參見 John Hrvatin,微軟,MIX09, http://videos.visitmix.com/MI...)。每次 ECMAScript 須要訪問 DOM 時,你須要過橋,交一次「過橋費」。你操做 DOM 次數越多,費用就越高。通常的建議是儘可能減小過橋次數,努力停留在 ECMAScript 島上。
訪問或修改元素最壞的狀況是使用循環執行此操做,特別是在 HTML 集合中使用循環算法
function innerHTMLLoop() { for (var count = 0; count < 15000; count++) { document.getElementById("here").innerHTML += "a"; } }
此函數在循環中更新頁面內容。這段代碼的問題是,在每次循環單元中都對 DOM 元素訪問兩次:一次
讀取 innerHTML 屬性能容,另外一次寫入它shell
優化以下編程
function innerHTMLLoop2() { var content = ""; for (var count = 0; count < 15000; count++) { content += "a"; } document.getElementById("here").innerHTML += content; }
你訪問 DOM 越多,代碼的執行速度就越慢。所以,通常經驗法則是:輕輕地觸摸 DOM,並儘可能保持在 ECMAScript 範圍內
使用 DOM 方法更新頁面內容的另外一個途徑是克隆已有 DOM 元素,而不是建立新的——即便用 element.cloneNode()(element 是一個已存在的節點)代替 document.createElement();
代碼總體結構是執行速度的決定因素之一。代碼量少不必定執行快,代碼量多,也不必定執行慢,性能損失與代碼組織方式和具體問題解決辦法直接相關。
在大多數編程語言中,代碼執行時間多數在循環中度過。在一系列編程模式中,循環是最多見的模式之一,提升性能必須控制好循環,死循環和長時間循環會嚴重影響用戶體驗。
前三種循環幾乎全部編程語言都能通用,for in 循環遍歷對象命名屬性(包括自有屬性和原型屬性)
循環性能爭論的源頭是應當選用哪一種循環,在 JS 中 for-in 比其餘循環明顯要慢(每次迭代都要搜索實例或原型屬性),除非對數目不詳的對象屬性進行操做,不然避免使用 for-in。除開 for-in,選擇循環應當基於需求而不是性能
減小每次迭代的操做總數能夠大幅提升循環的總體性能
優化循環:
編程中常常會聽到此說法,如今來驗證一下,測試樣例
var arr = []; for (var i = 0; i < 100000000; i++) { arr[i] = i; } var start = +new Date(); for (var j = arr.length; j > -1; j--) { arr[j] = j; } console.log("倒序循環耗時:%s ms", Date.now() - start); //約180 ms var start = +new Date(); for (var j = 0; j < arr.length; j++) { arr[j] = j; } console.log("正序序循環耗時:%s ms", Date.now() - start); //約788 ms
儘管基於函數的迭代顯得更加便利,它仍是比基於循環的迭代要慢一些。每一個數組項要關聯額外的函數調用是形成速度慢的緣由。在全部狀況下,基於函數的迭代佔用時間是基於循環的迭代的八倍,所以在關注執行時間的狀況下它並非一個合適的辦法。
使用 if-else 或者 switch 的流行理論是基於測試條件的數量:條件數量較大,傾向使用 switch,更易於閱讀
當條件體增長時,if-else 性能負擔增長的程度比 switch 更多。
通常來講,if-else 適用於判斷兩個離散的值或者幾個不一樣的值域,若是判斷條件較多 switch 表達式將是更理想的選擇
會受瀏覽器調用棧大小的限制
任何能夠用遞歸實現的算法能夠用迭代實現。使用優化的循環替代長時間運行的遞歸函數能夠提升性能,由於運行一個循環比反覆調用一個函數的開銷要低
斐波那契
function fibonacci(n) { if (n === 1) return 1; if (n === 2) return 2; return fibonacci(n - 1) + fibonacci(n - 2); }
//製表 function memorize(fundamental, cache) { cache = cache || {}; var shell = function(args) { if (!cache.hasOwnProperty(args)) { cache[args] = fundamental(args); } return cache[args]; }; return shell; } //動態規劃 function fibonacciOptimize(n) { if (n === 1) return 1; if (n === 2) return 2; var current = 2; var previous = 1; for (var i = 3; i <= n; i++) { var temp = current; current = previous + current; previous = temp; } return current; } //計算階乘 var res1 = fibonacci(40); var res2 = memorize(fibonacci)(40); var res3 = fibonacciOptimize(40); //計算出來的res3優於res2,res2優於res1
運行代碼的總量越大,優化帶來的性能提高越明顯
正如其餘編程語言,代碼的寫法與算法選用影響 JS 的運行時間,與其餘編程語言不一樣,JS 可用資源有限,因此優化當然重要
在 JS 中,正則是必不可少的東西,它的重要性遠遠超過煩瑣的字符串處理
字符串鏈接表現出驚人的性能緊張。一般一個任務經過一個循環,向字符串末尾不斷地添加內容,來建立一個字符串(例如,建立一個 HTML 表或者一個 XML 文檔),但此類處理在一些瀏覽器上表現糟糕而遭人痛恨
Method | Example |
---|---|
+ | str = 'a' + 'b' + 'c'; |
+= | str = 'a'; str += 'b'; str += 'c'; |
array.join() | str = ['a','b','c'].join(''); |
string.concat() | str = 'a'; str = str.concat('b', 'c'); |
當鏈接少許的字符串,上述的方式都很快,可根據本身的習慣使用;
當合並字符串的長度和數量增長以後,有些函數就開始發揮其做用了
str += "a" + "b";
此代碼執行時,發生四個步驟
下面的代碼經過兩個離散的表達式直接將內容附加在 str 上避免了臨時字符串
str += "a"; str += "b";
事實上用一行代碼就能夠解決
str = str + "a" + "b";
賦值表達式以 str 開頭,一次追加一個字符串,從左至右依次鏈接。若是改變了鏈接順序(例如:str = 'a' + str + 'b'
),你會失去這種優化,這與瀏覽器合併字符串時分配內存的方法有關。除 IE 外,瀏覽器嘗試擴展表達式左端字符串的內存,而後簡單地將第二個字符串拷貝到它的尾部。若是在一個循環中,基本字符串在左端,能夠避免屢次複製一個愈來愈大的基本字符串。
Array.prototype.join 將數組的全部元素合併成一個字符串,並在每一個元素之間插入一個分隔符字符串。若傳遞一個空字符串,可將數組的全部元素簡單的拼接起來
var start = Date.now(); var str = "I'm a thirty-five character string.", newStr = "", appends = 5000000; while (appends--) { newStr += str; } var time = Date.now() - start; console.log("耗時:" + time + "ms"); //耗時:1360ms
var start = Date.now(); var str = "I'm a thirty-five character string.", strs = [], newStr = "", appends = 5000000; while (appends--) { strs[strs.length] = str; } newStr = strs.join(""); var time = Date.now() - start; console.log("耗時:" + time + "ms"); //耗時:414ms
這一難以置信的改進結果是由於避免了重複的內存分配和拷貝愈來愈大的字符串。
原生字符串鏈接函數接受任意數目的參數,並將每個參數都追加在調用函數的字符串上
var str = str.concat(s1); var str = str.concat(s1, s2, s3); var str = String.prototype.concat.apply(str, array);
大多數狀況下 concat 比簡單的+或+=慢一些
許多因素影響正則表達式的效率,首先,正則適配的文本千差萬別,部分匹配時比徹底不匹配所用的時間要長,每種瀏覽器的正則引擎也有不一樣的內部優化
在大多數現代正則表達式實現中(包括 JavaScript 所需的),回溯是匹配過程的基本組成部分。它很大程度上也是正則表達式如此美好和強大的根源。然而,回溯計算代價昂貴,若是你不夠當心的話容易失控。雖然回溯是總體性能的惟一因素,理解它的工做原理,以及如何減小使用頻率,多是編寫高效正則表達式最重要的關鍵點。
正則表達式匹配過程
- 當一個正則表達式掃描目標字符串時,它從左到右逐個掃描正則表達式的組成部分,在每一個位置上測試能不能找到一個匹配。對於每個量詞和分支,都必須決定如何繼續進行。若是是一個量詞(諸如*,+?,或者{2,}),正則表達式必須決定什麼時候嘗試匹配更多的字符;若是遇到分支(經過|操做符),它必須從這些選項中選擇一個進行嘗試。
- 每當正則表達式作出這樣的決定,若是有必要的話,它會記住另外一個選項,以備未來返回後使用。若是所選方案匹配成功,正則表達式將繼續掃描正則表達式模板,若是其他部分匹配也成功了,那麼匹配就結束了。可是若是所選擇的方案未能發現相應匹配,或者後來的匹配也失敗了,正則表達式將回溯到最後一個決策點,而後在剩餘的選項中選擇一個。它繼續這樣下去,直到找到一個匹配,或者量詞和分支選項的全部可能的排列組合都嘗試失敗了,那麼它將放棄這一過程,而後移動到此過程開始位置的下一個字符上,重複此過程。
示例分析
/h(ello|appy) hippo/.test("hello there, happy hippo");
此正則表達式匹配「hello hippo」或「happy hippo」。測試一開始,它要查找一個 h,目標字符串的第一個字母剛好就是 h,它馬上就被找到了。接下來,子表達式(ello|appy)提供了兩個處理選項。正則表達式選擇最左邊的選項(分支選擇老是從左到右進行),檢查 ello 是否匹配字符串的下一個字符。確實匹配,而後正則表達式又匹配了後面的空格。然而在這一點上它走進了死衚衕,由於 hippo 中的 h 不能匹配字符串中的下一個字母 t。此時正則表達式還不能放棄,由於它尚未嘗試過全部的選擇,隨後它回溯到最後一個檢查點(在它匹配了首字母 h 以後的那個位置上)並嘗試匹配第二個分支選項。可是沒有成功,並且也沒有更多的選項了,因此正則表達式認爲從字符串的第一個字符開始匹配是不能成功的,所以它從第二個字符開始,從新進行查找。它沒有找到 h,因此就繼續向後找,直到第 14 個字母才找到,它匹配 happy 的那個 h。而後它再次進入分支過程。此次 ello 未能匹配,可是回溯以後第二次分支過程當中,它匹配了整個字符串「happy hippo」(如圖 5-4)。匹配成功了。
當一個正則表達式佔用瀏覽器上秒,上分鐘或者更長時間時,問題緣由極可能是回溯失控。正則表達式處理慢每每是由於匹配失敗過程慢,而不是匹配成功過程慢。
var reg = /<html>[\s\S]*?<head>[\s\S]*?<title>[\s\S]*?<\/title>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/; //優化以下 var regOptimize = /<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/;
如今若是沒有尾隨的</html>那麼最後一個[sS]*?將擴展至字符串結束,正則表達式將馬上失敗由於沒有回溯點能夠返回
var endsWithSemicolon = /;$/.test(str);
你可能以爲很奇怪,雖然說當前沒有哪一個瀏覽器聰明到這個程度,可以意識到這個正則表達式只能匹配字符串的末尾。最終它們所作的將是一個一個地測試了整個字符串。字符串的長度越長(包含的分號越多),它佔用的時間也越長
var endsWithSemicolon = str.charAt(str.length - 1) == ";";
這種狀況下,更好的辦法是跳過正則表達式所需的全部中間步驟,簡單地檢查最後一個字符是否是分號:
這個例子使用 charAt 函數在特定位置上讀取字符。字符串函數 slice,substr,和 substring 可用於在特定位置上提取並檢查字符串的值
全部這些字符串操做函數速度都很快,當您搜索那些不依賴正則表達式複雜特性的文本字符串時,它們有助於您避免正則表達式帶來的性能開銷
正則表達式容許你用不多的代碼實現一個修剪函數,這對 JavaScript 關心文件大小的庫來講十分重要。可能最好的全面解決方案是使用兩個子表達式:一個用於去除頭部空格,另外一個用於去除尾部空格。這樣處理簡單而迅速,特別是處理長字符串時。
//方法 用正則表達式修剪 // trim1 String.prototype.trim = function() { return this.replace(/^\s+/, "").replace(/\s+$/, ""); }; //trim2 String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); }; // trim 3 String.prototype.trim = function() { return this.replace(/^\s*([\s\S]*?)\s*$/, "$1"); }; // trim 4 String.prototype.trim = function() { return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1"); }; // trim 5 String.prototype.trim = function() { return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1"); }; //方法二 不使用正則表達式修剪 String.prototype.trim = function() { var start = 0; var end = this.length - 1; //ws 變量包括 ECMAScript 5 中定義的全部空白字符 var ws = "\n\r\t\f\x0b\xa0\u1680\u180e\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); }; //方法三 混合解決方案 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); };
簡單地使用兩個子正則表達式在全部瀏覽器上處理不一樣內容和長度的字符串時,均表現出穩定的性能。所以它能夠說是最全面的解決方案。混合解決方案在處理長字符串時特別快,其代價是代碼稍長,在某些瀏覽器上處理尾部長空格時存在弱點
用戶傾向於重複嘗試這些不發生明顯變化的動做,因此確保網頁應用程序的響應速度也是一個重要的性能關注點
JavaScript 和 UI 更新共享的進程一般被稱做瀏覽器 UI 線程, UI 線程圍繞着一個簡單的隊列系統工做,任務被保存到隊列中直至進程空閒。一旦空閒,隊列中的下一個任務將被檢索和運行。這些任務不是運行 JavaScript 代碼,就是執行 UI 更新,包括重繪和重排版.
大多數瀏覽器在 JavaScript 運行時中止 UI 線程隊列中的任務,也就是說 JavaScript 任務必須儘快結束,以避免對用戶體驗形成不良影響
Brendan Eich,JavaScript 的創造者,引用他的話說,「[JavaScript]運行了整整幾秒鐘極可能是作錯了什麼……」
定時器與 UI 線程交互的方式有助於分解長運行腳本成爲較短的片段
全部瀏覽器試圖儘量準確,但一般會發生幾毫秒滑移,或快或慢。正由於這個緣由,定時器不可用於測量實際時間
目前最經常使用的方法中,XMLHttpRequest(XHR)用來異步收發數據。全部現代瀏覽器都可以很好地支持它,並且可以精細地控制發送請求和數據接收。你能夠向請求報文中添加任意的頭信息和參數(包括 GET 和 POST),並讀取從服務器返回的頭信息,以及響應文本自身
五種經常使用技術用於向服務器請求數據
//封裝ajax var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status >= 200) { // } }; xhr.open(type, url, true); xhr.setRequestHeader("Content-Type", contentType); xhr.send(null);
經過 Douglas Crockford 的發明與推廣,JSON 是一個輕量級並易於解析的數據格式,它按照 JavaScript 對象和數組字面語法所編寫
數據傳輸技術和數據格式
高性能 Ajax 包括:知道你項目的具體需求,選擇正確的數據格式和與之相配的傳輸技術
封裝本身的 ajax 庫
(function(root) { root.MyAjax = (config = {}) => { let url = config.url; let type = config.type || "GET"; let async = config.async || true; let headers = config.headers || []; let contentType = config.contentType || "application/json;charset=utf-8"; let data = config.data; let dataType = config.dataType || "json"; let successFn = config.success; let errorFn = config.error; let completeFn = config.complete; let xhr; if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { let rsp = xhr.responseText || xhr.responseXML; if (dataType === "json") { rsp = eval("(" + rsp + ")"); } successFn(rsp, xhr.statusText, xhr); } else { errorFn(xhr.statusText, xhr); } if (completeFn) { completeFn(xhr.statusText, xhr); } } }; xhr.open(type, url, async); //設置超時 if (async) { xhr.timeout = config.timeout || 0; } //設置請求頭 for (let i = 0; i < headers.length; ++i) { xhr.setRequestHeader(headers[i].name, headers[i].value); } xhr.setRequestHeader("Content-Type", contentType); //send if ( typeof data == "object" && contentType === "application/x-www-form-urlencoded" ) { let s = ""; for (attr in data) { s += attr + "=" + data[attr] + "&"; } if (s) { s = s.slice(0, s.length - 1); } xhr.send(s); } else { xhr.send(data); } }; })(window);
位操做運算符
四種位邏輯操做符
num % 2 === 0; //取模與0進行判斷 num & 1; //位與1結果位1則爲奇數,爲0則爲偶數
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;
可使用位與操做檢查一個給定的選項是否可用
//is option A in the list? if (options & OPTION_A) { //do something } //is option B in the list? if (options & OPTION_B) { //do something }
像這樣的位掩碼操做很是快,正由於前面提到的緣由,操做發生在系統底層。若是許多選項保存在一塊兒並常常檢查,位掩碼有助於加快總體性能
不管你怎樣優化 JavaScript 代碼,它永遠不會比 JavaScript 引擎提供的原生方法更快。經驗不足的 JavaScript 開發者常常犯的一個錯誤是在代碼中進行復雜的數學運算,而沒有使用內置 Math 對象中那些性能更好的版本。Math 對象包含專門設計的屬性和方法,使數學運算更容易。
//查看Math對象全部方法 Object.getOwnPropertyNames(Math);
當網頁或應用程序變慢時,分析網上傳來的資源,分析腳本的運行性能,使你可以集中精力在那些須要努力優化的地方。
能讀到最後的同窗也不容易,畢竟篇幅稍長。本書大概花了三週的零碎時間讀完,建議你們讀一讀。若是你們在看書過程當中存在疑問,不妨打開電腦驗證書中做者的言論,或許會更加深入。
若文中有錯誤歡迎你們評論指出,或者加我微信好友一塊兒交流
gm4118679254