亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

編寫高性能JavaScript(譯)

2019-11-20 14:16:51
字體:
來源:轉載
供稿:網友

譯者按:本人第一次翻譯外文,言語難免有些晦澀,但盡量表達了作者的原意,未經過多的潤色,歡迎批評指正。另本文篇幅較長、信息量大,可能難以消化,歡迎留言探討細節問題。本文主要關注V8的性能優化,部分內容并不適用于所有JS引擎。最后,轉載請注明出處: )

========================譯文分割線===========================

很多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是專門為需要快速執行的大型JavaScript應用所設計的。如果你是一個開發者,并且關心內存使用情況與頁面性能,你應該了解用戶瀏覽器中的JavaScript引擎是如何運作的。無論是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其他引擎,這樣做可以幫助你更好地優化你的應用程序。這并不是說應該專門為某一瀏覽器或引擎做優化,千萬別這么做。

但是,你應該問自己幾個問題:

  • 在我的代碼里,是否可以使代碼更高效一些
  • 主流的JavaScript引擎都做了哪些優化
  • 什么是引擎無法優化的,垃圾回收器(GC)是否能回收我所期望的東西

fast_memory

加載快速的網站就像是一輛快速的跑車,需要用到特別定制的零件. 圖片來源: dHybridcars.

編寫高性能代碼時有一些常見的陷阱,在這篇文章中,我們將展示一些經過驗證的、更好的編寫代碼方式。

那么,JavaScript在V8里是如何工作的?

如果你對JS引擎沒有較深的了解,開發一個大型Web應用也沒啥問題,就好比會開車的人也只是看過引擎蓋而沒有看過車蓋內的引擎一樣。鑒于Chrome是我的瀏覽器首選,所以談一下它的JavaScript引擎。V8是由以下幾個核心部分組成:

  • 一個基本的編譯器,它會在代碼執行前解析JavaScript代碼并生成本地機器碼,而不是執行字節碼或簡單地解釋它。這些代碼最開始并不是高度優化的。
  • V8將對象構建為對象模型。在JavaScript中對象表現為關聯數組,但是在V8中對象被看作是隱藏的類,一個為了優化查詢的內部類型系統。
  • 運行時分析器監視正在運行的系統,并標識了“hot”的函數(例如花費很長時間運行的代碼)。
  • 優化編譯器重新編譯和優化那些被運行時分析器標識為“hot”的代碼,并進行“內聯”等優化(例如用被調用者的主體替換函數調用的位置)。
  • V8支持去優化,這意味著優化編譯器如果發現對于代碼優化的假設過于樂觀,它會舍棄優化過的代碼。
  • V8有個垃圾收集器,了解它是如何工作的和優化JavaScript一樣重要。

垃圾回收

垃圾回收是內存管理的一種形式,其實就是一個收集器的概念,嘗試回收不再被使用的對象所占用的內存。在JavaScript這種垃圾回收語言中,應用程序中仍在被引用的對象不會被清除。

手動消除對象引用在大多數情況下是沒有必要的。通過簡單地把變量放在需要它們的地方(理想情況下,盡可能是局部作用域,即它們被使用的函數里而不是函數外層),一切將運作地很好。

robot-cleaner

垃圾回收器嘗試回收內存. 圖片來源: Valtteri Mäki.

在JavaScript中,是不可能強制進行垃圾回收的。你不應該這么做,因為垃圾收集過程是由運行時控制的,它知道什么是最好的清理時機。

“消除引用”的誤解

網上有許多關于JavaScript內存回收的討論都談到delete這個關鍵字,雖然它可以被用來刪除對象(map)中的屬性(key),但有部分開發者認為它可以用來強制“消除引用”。建議盡可能避免使用delete,在下面的例子中delete o.x 的弊大于利,因為它改變了o的隱藏類,并使它成為一個"慢對象"。

var o = { x: 1 }; delete o.x; // true o.x; // undefined

你會很容易地在流行的JS庫中找到引用刪除――這是具有語言目的性的。這里需要注意的是避免在運行時修改”hot”對象的結構。JavaScript引擎可以檢測出這種“hot”的對象,并嘗試對其進行優化。如果對象在生命周期中其結構沒有較大的改變,引擎將會更容易優化對象,而delete操作實際上會觸發這種較大的結構改變,因此不利于引擎的優化。

對于null是如何工作也是有誤解的。將一個對象引用設置為null,并沒有使對象變“空”,只是將它的引用設置為空而已。使用o.x= null比使用delete會更好些,但可能也不是很必要。

var o = { x: 1 }; o = null;o; // nullo.x // TypeError

如果此引用是當前對象的最后引用,那么該對象將被作為垃圾回收。如果此引用不是當前對象的最后引用,則該對象是可訪問的且不會被垃圾回收。

另外需要注意的是,全局變量在頁面的生命周期里是不被垃圾回收器清理的。無論頁面打開多久,JavaScript運行時全局對象作用域中的變量會一直存在。

var myGlobalNamespace = {};

全局對象只會在刷新頁面、導航到其他頁面、關閉標簽頁或退出瀏覽器時才會被清理。函數作用域的變量將在超出作用域時被清理,即退出函數時,已經沒有任何引用,這樣的變量就被清理了。

經驗法則

為了使垃圾回收器盡早收集盡可能多的對象,不要hold著不再使用的對象。這里有幾件事需要記住:

  • 正如前面提到的,在合適的范圍內使用變量是手動消除引用的更好選擇。即一個變量只在一個函數作用域中使用,就不要在全局作用域聲明它。這意味著更干凈省心的代碼。
  • 確保解綁那些不再需要的事件監聽器,尤其是那些即將被銷毀的DOM對象所綁定的事件監聽器。
  • 如果使用的數據緩存在本地,確保清理一下緩存或使用老化機制,以避免大量不被重用的數據被存儲。

函數

接下來,我們談談函數。正如我們已經說過,垃圾收集的工作原理,是通過回收不再是訪問的內存塊(對象)。為了更好地說明這一點,這里有一些例子。

function foo() { var bar = new LargeObject(); bar.someCall();}

當foo返回時,bar指向的對象將會被垃圾收集器自動回收,因為它已沒有任何存在的引用了。

對比一下:

function foo() { var bar = new LargeObject(); bar.someCall(); return bar;}// somewhere elsevar b = foo();

現在我們有一個引用指向bar對象,這樣bar對象的生存周期就從foo的調用一直持續到調用者指定別的變量b(或b超出范圍)。

閉包(CLOSURES)

當你看到一個函數,返回一個內部函數,該內部函數將獲得范圍外的訪問權,即使在外部函數執行之后。這是一個基本的閉包 ―― 可以在特定的上下文中設置的變量的表達式。例如:

function sum (x) { function sumIt(y) {  return x + y; }; return sumIt;}// Usagevar sumA = sum(4);var sumB = sumA(3);console.log(sumB); // Returns 7

在sum調用上下文中生成的函數對象(sumIt)是無法被回收的,它被全局變量(sumA)所引用,并且可以通過sumA(n)調用。

讓我們來看看另外一個例子,這里我們可以訪問變量largeStr嗎?

var a = function () { var largeStr = new Array(1000000).join('x'); return function () {  return largeStr; };}();

是的,我們可以通過a()訪問largeStr,所以它沒有被回收。下面這個呢?

var a = function () { var smallStr = 'x'; var largeStr = new Array(1000000).join('x'); return function (n) {  return smallStr; };}();

我們不能再訪問largeStr了,它已經是垃圾回收候選人了?!咀g者注:因為largeStr已不存在外部引用了】

定時器

最糟的內存泄漏地方之一是在循環中,或者在setTimeout()/ setInterval()中,但這是相當常見的。思考下面的例子:

var myObj = { callMeMaybe: function () {  var myRef = this;  var val = setTimeout(function () {    console.log('Time is running out!');    myRef.callMeMaybe();  }, 1000); }};

如果我們運行myObj.callMeMaybe();來啟動定時器,可以看到控制臺每秒打印出“Time is running out!”。如果接著運行myObj = null,定時器依舊處于激活狀態。為了能夠持續執行,閉包將myObj傳遞給setTimeout,這樣myObj是無法被回收的。相反,它引用到myObj的因為它捕獲了myRef。這跟我們為了保持引用將閉包傳給其他的函數是一樣的。

同樣值得牢記的是,setTimeout/setInterval調用(如函數)中的引用,將需要執行和完成,才可以被垃圾收集。

當心性能陷阱

永遠不要優化代碼,直到你真正需要?,F在經常可以看到一些基準測試,顯示N比M在V8中更為優化,但是在模塊代碼或應用中測試一下會發現,這些優化真正的效果比你期望的要小的多。

speed-trap

做的過多還不如什么都不做. 圖片來源: Tim Sheerman-Chase.

比如我們想要創建這樣一個模塊:

  • 需要一個本地的數據源包含數字ID
  • 繪制包含這些數據的表格
  • 添加事件處理程序,當用戶點擊的任何單元格時切換單元格的css class

這個問題有幾個不同的因素,雖然也很容易解決。我們如何存儲數據,如何高效地繪制表格并且append到DOM中,如何更優地處理表格事件?

面對這些問題最開始(天真)的做法是使用對象存儲數據并放入數組中,使用jQuery遍歷數據繪制表格并append到DOM中,最后使用事件綁定我們期望地點擊行為。

注意:這不是你應該做的

var moduleA = function () { return {  data: dataArrayObject,  init: function () {   this.addTable();   this.addEvents();  },  addTable: function () {   for (var i = 0; i < rows; i++) {    $tr = $('<tr></tr>');    for (var j = 0; j < this.data.length; j++) {     $tr.append('<td>' + this.data[j]['id'] + '</td>');    }    $tr.appendTo($tbody);   }  },  addEvents: function () {   $('table td').on('click', function () {    $(this).toggleClass('active');   });  } };}();

這段代碼簡單有效地完成了任務。

但在這種情況下,我們遍歷的數據只是本應該簡單地存放在數組中的數字型屬性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以這種方式)來生成表格是更優的選擇,當然,事件代理比單獨綁定每個td具有更高的性能。

要注意雖然jQuery在內部使用DocumentFragment,但是在我們的例子中,代碼在循環內調用append并且這些調用涉及到一些其他的小知識,因此在這里起到的優化作用不大。希望這不會是一個痛點,但請務必進行基準測試,以確保自己代碼ok。

對于我們的例子,上述的做法帶來了(期望的)性能提升。事件代理對簡單的綁定是一種改進,可選的DocumentFragment也起到了助推作用。

var moduleD = function () { return {  data: dataArray,  init: function () {   this.addTable();   this.addEvents();  },  addTable: function () {   var td, tr;   var frag = document.createDocumentFragment();   var frag2 = document.createDocumentFragment();   for (var i = 0; i < rows; i++) {    tr = document.createElement('tr');    for (var j = 0; j < this.data.length; j++) {     td = document.createElement('td');     td.appendChild(document.createTextNode(this.data[j]));     frag2.appendChild(td);    }    tr.appendChild(frag2);    frag.appendChild(tr);   }   tbody.appendChild(frag);  },  addEvents: function () {   $('table').on('click', 'td', function () {    $(this).toggleClass('active');   });  } };}();

接下來看看其他提升性能的方式。你也許曾經在哪讀到過使用原型模式比模塊模式更優,或聽說過使用JS模版框架性能更好。有時的確如此,不過使用它們其實是為了代碼更具可讀性。對了,還有預編譯!讓我們看看在實踐中表現的如何?

moduleG = function () {};moduleG.prototype.data = dataArray;moduleG.prototype.init = function () { this.addTable(); this.addEvents();};moduleG.prototype.addTable = function () { var template = _.template($('#template').text()); var html = template({'data' : this.data}); $tbody.append(html);};moduleG.prototype.addEvents = function () { $('table').on('click', 'td', function () {  $(this).toggleClass('active'); });};var modG = new moduleG();

事實證明,在這種情況下的帶來的性能提升可以忽略不計。模板和原型的選擇并沒有真正提供更多的東西。也就是說,性能并不是開發者使用它們的原因,給代碼帶來的可讀性、繼承模型和可維護性才是真正的原因。

更復雜的問題包括高效地在canvas上繪制圖片和操作帶或不帶類型數組的像素數據。

在將一些方法用在你自己的應用之前,一定要多了解這些方案的基準測試。也許有人還記得JS模版的shoot-off隨后的擴展版。你要搞清楚基準測試不是存在于你看不到的那些虛擬應用,而是應該在你的實際代碼中去測試帶來的優化。

V8優化技巧

詳細介紹了每個V8引擎的優化點在本文討論范圍之外,當然這里也有許多值得一提的技巧。記住這些技巧你就能減少那些性能低下的代碼了。