V8 是由 Google 開發的開源 JavaScript 引擎,也被稱爲虛擬機,模擬實際計算機各類功能來實現代碼的編譯和執行。javascript
上圖清晰版
咱們寫的 JavaScript 代碼直接交給瀏覽器或者 Node 執行時,底層的 CPU 是不認識的,也無法執行。CPU 只認識本身的指令集,指令集對應的是彙編代碼。寫彙編代碼是一件很痛苦的事情。而且不一樣類型的 CPU 的指令集是不同的,那就意味着須要給每一種 CPU 重寫彙編代碼。
JavaScirpt 引擎能夠將 JS 代碼編譯爲不一樣 CPU(Intel, ARM 以及 MIPS 等)對應的彙編代碼,這樣咱們就不須要去翻閱每一個 CPU 的指令集手冊來編寫彙編代碼了。固然,JavaScript 引擎的工做也不僅是編譯代碼,它還要負責執行代碼、分配內存以及垃圾回收。html
1000100111011000 #機器指令 mov ax,bx #彙編指令
資料拓展: 彙編語言入門教程【阮一峯】 | 理解 V8 的字節碼「譯」
Google V8 引擎是用 C ++編寫的開源高性能 JavaScript 和 WebAssembly 引擎,它已被用於 Chrome 和 Node.js 等。能夠運行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統上。 V8 最先被開發用以嵌入到 Google 的開源瀏覽器 Chrome 中,第一個版本隨着初版Chrome於 2008 年 9 月 2 日發佈。可是 V8 是一個能夠獨立運行的模塊,徹底能夠嵌入到任何 C ++應用程序中。著名的 Node.js( 一個異步的服務器框架,能夠在服務端使用 JavaScript 寫出高效的網絡服務器 ) 就是基於 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。前端
和其餘 JavaScript 引擎同樣,V8 會編譯 / 執行 JavaScript 代碼,管理內存,負責垃圾回收,與宿主語言的交互等。經過暴露宿主對象 ( 變量,函數等 ) 到 JavaScript,JavaScript 能夠訪問宿主環境中的對象,並在腳本中完成對宿主對象的操做。html5
資料拓展: v8 logo | V8 (JavaScript engine) | 《V八、JavaScript+的如今與將來》 | 幾張圖讓你看懂 WebAssembly
d8 是一個很是有用的調試工具,你能夠把它當作是 debug for V8 的縮寫。咱們可使用 d8 來查看 V8 在執行 JavaScript 過程當中的各類中間數據,好比做用域、AST、字節碼、優化的二進制代碼、垃圾回收的狀態,還可使用 d8 提供的私有 API 查看一些內部信息。java
方法一:自行下載編譯node
方法二:使用編譯好的 d8 工具linux
// 解壓文件,點擊d8打開(mac安全策略限制的話,按住control,再點擊,彈出菜單中選擇打開) V8 version 8.4.109 d8> 1 + 2 3 d8> 2 + '4' "24" d8> console.log(23) 23 undefined d8> var a = 1 undefined d8> a + 2 3 d8> this [object global] d8>
本文後續用於 demo 演示時的文件目錄結構:c++
V8: # d8可執行文件 d8 icudtl.dat libc++.dylib libchrome_zlib.dylib libicui18n.dylib libicuuc.dylib libv8.dylib libv8_debug_helper.dylib libv8_for_testing.dylib libv8_libbase.dylib libv8_libplatform.dylib obj snapshot_blob.bin v8_build_config.json # 新建的js示例文件 test.js
方法三:macgit
# 若是已有HomeBrew,忽略第一條命令 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install v8
node --print-bytecode ./test.js
,打印出 Ignition(解釋器)生成的 Bytecode(字節碼)。查看 d8 命令程序員
# 若是不想使用./d8這種方式進行調試,可將d8加入環境變量,以後就能夠直接`d8 --help`了 ./d8 --help
過濾特定的命令
# 若是是 Windows 系統,可能缺乏 grep 程序,請自行下載安裝並添加環境變量 ./d8 --help |grep print
如:
// test.js function sum(a) { var b = 6; return a + 6; } console.log(sum(3));
# d8 後面跟上文件名和要執行的命令,如執行下面這行命令,就會打印出 test.js 文件所生成的字節碼。 ./d8 ./test.js --print-bytecode # 執行如下命令,輸出9 ./d8 ./test.js
你還可使用 V8 所提供的一些內部方法,只須要在啓動 V8 時傳入 --allow-natives-syntax
命令,你就能夠在 test.js 中使用諸如HasFastProperties
(檢查一個對象是否擁有快屬性)的內部方法(索引屬性、常規屬性、快屬性等下文會介紹)。
function Foo(property_num, element_num) { //添加可索引屬性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常規屬性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10); // 檢查一個對象是否擁有快屬性 console.log(%HasFastProperties(bar)); delete bar.property2; console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js # 依次打印:true false
V8 是一個很是複雜的項目,有超過 100 萬行 C++代碼。它由許多子模塊構成,其中這 4 個模塊是最重要的:
Ignition:interpreter,即解釋器,負責將 AST 轉換爲 Bytecode,解釋執行 Bytecode;同時收集 TurboFan 優化編譯所需的信息,好比函數參數的類型;解釋器執行時主要有四個模塊,內存中的字節碼、寄存器、棧、堆。
一般有兩種類型的解釋器,基於棧 (Stack-based)和基於寄存器 (Register-based),基於棧的解釋器使用棧來保存函數參數、中間運算結果、變量等;基於寄存器的虛擬機則支持寄存器的指令操做,使用寄存器來保存參數、中間計算結果。一般,基於棧的虛擬機也定義了少許的寄存器,基於寄存器的虛擬機也有堆棧,其 區別體如今它們提供的指令集體系。 大多數解釋器都是基於棧的,好比 Java 虛擬機,.Net 虛擬機,還有早期的 V8 虛擬機。基於堆棧的虛擬機在處理函數調用、解決遞歸問題和切換上下文時簡單明快。而 如今的 V8 虛擬機則採用了基於寄存器的設計,它將一些中間數據保存到寄存器中。
基於寄存器的解釋器架構:![]()
資料參考: 解釋器是如何解釋執行字節碼的?
其中,Parser,Ignition 以及 TurboFan 能夠將 JS 源碼編譯爲彙編代碼,其流程圖以下:
簡單地說,Parser 將 JS 源碼轉換爲 AST,而後 Ignition 將 AST 轉換爲 Bytecode,最後 TurboFan 將 Bytecode 轉換爲通過優化的 Machine Code(其實是彙編代碼)。
圖片中的紅色虛線是逆向的,也就是說Optimized Machine Code 會被還原爲 Bytecode,這個過程叫作 Deoptimization。這是由於 Ignition 收集的信息多是錯誤的,好比 add 函數的參數以前是整數,後來又變成了字符串。生成的 Optimized Machine Code 已經假定 add 函數的參數是整數,那固然是錯誤的,因而須要進行 Deoptimization。
function add(x, y) { return x + y; } add(3, 5); add('3', '5');
在運行 C、C++以及 Java 等程序以前,須要進行編譯,不能直接執行源碼;但對於 JavaScript 來講,咱們能夠直接執行源碼(好比:node test.js),它是在運行的時候先編譯再執行,這種方式被稱爲即時編譯(Just-in-time compilation),簡稱爲 JIT。所以,V8 也屬於 JIT 編譯器。
資料拓展參考: V8 引擎是如何工做的?
V8 執行一段 JavaScript 的流程圖:
資料拓展: V8 是如何執行一段 JavaScript 代碼的?
V8 本質上是一個虛擬機,由於計算機只能識別二進制指令,因此要讓計算機執行一段高級語言一般有兩種手段:
總結:
V8 執行一段 JavaScript 代碼所經歷的主要流程包括:
// 閉包(靜態做用域,一等公民,調用棧的矛盾體) function foo() { var d = 20; return function inner(a, b) { const c = a + b + d; return c; }; } const f = foo();
關於閉包,可參考我之前的一篇文章,在此再也不贅述,在此主要談下閉包給 Chrome V8 帶來的問題及其解決策略。
所謂惰性解析是指解析器在解析的過程當中,若是遇到函數聲明,那麼會跳過函數內部的代碼,並不會爲其生成 AST 和字節碼,而僅僅生成頂層代碼的 AST 和字節碼。
在編譯 JavaScript 代碼的過程當中,V8 並不會一次性將全部的 JavaScript 解析爲中間代碼,這主要是基於如下兩點:
V8 引入預解析器,好比當解析頂層代碼的時候,遇到了一個函數,那麼預解析器並不會直接跳過該函數,而是對該函數作一次快速的預解析。
下面的代碼會輸出什麼:
// test.js function Foo() { this[200] = 'test-200'; this[1] = 'test-1'; this[100] = 'test-100'; this['B'] = 'bar-B'; this[50] = 'test-50'; this[9] = 'test-9'; this[8] = 'test-8'; this[3] = 'test-3'; this[5] = 'test-5'; this['D'] = 'bar-D'; this['C'] = 'bar-C'; } var bar = new Foo(); for (key in bar) { console.log(`index:${key} value:${bar[key]}`); } //輸出: // index:1 value:test-1 // index:3 value:test-3 // index:5 value:test-5 // index:8 value:test-8 // index:9 value:test-9 // index:50 value:test-50 // index:100 value:test-100 // index:200 value:test-200 // index:B value:bar-B // index:D value:bar-D // index:C value:bar-C
在ECMAScript 規範中定義了數字屬性應該按照索引值大小升序排列,字符串屬性根據建立時的順序升序排列。在這裏咱們把對象中的數字屬性稱爲排序屬性,在 V8 中被稱爲 elements,字符串屬性就被稱爲常規屬性,在 V8 中被稱爲 properties。在 V8 內部,爲了有效地提高存儲和訪問這兩種屬性的性能,分別使用了兩個線性數據結構來分別保存排序屬性和常規屬性。同時 v8 將部分常規屬性直接存儲到對象自己,咱們把這稱爲對象內屬性 (in-object properties),不過對象內屬性的數量是固定的,默認是 10 個。
function Foo(property_num, element_num) { //添加可索引屬性 for (let i = 0; i < element_num; i++) { this[i] = `element${i}`; } //添加常規屬性 for (let i = 0; i < property_num; i++) { let ppt = `property${i}`; this[ppt] = ppt; } } var bar = new Foo(10, 10);
能夠經過 Chrome 開發者工具的 Memory 標籤,捕獲查看當前的內存快照。經過增大第一個參數來查看存儲變化。
咱們將保存在線性數據結構中的屬性稱之爲「快屬性」,由於線性數據結構中只須要經過索引便可以訪問到屬性,雖然訪問線性結構的速度快,可是若是從線性結構中添加或者刪除大量的屬性時,則執行效率會很是低,這主要由於會產生大量時間和內存開銷。所以,若是一個對象的屬性過多時,V8 就會採起另一種存儲策略,那就是「慢屬性」策略,但慢屬性的對象內部會有獨立的非線性數據結構 (字典) 做爲屬性存儲容器。全部的屬性元信息再也不是線性存儲的,而是直接保存在屬性字典中。
v8 屬性存儲:
總結:
由於 JavaScript 中的對象是由一組組屬性和值組成的,因此最簡單的方式是使用一個字典來保存屬性和值,可是因爲字典是非線性結構,因此若是使用字典,讀取效率會大大下降。爲了提高查找效率,V8 在對象中添加了兩個隱藏屬性,排序屬性和常規屬性,element 屬性指向了 elements 對象,在 elements 對象中,會按照順序存放排序屬性。properties 屬性則指向了 properties 對象,在 properties 對象中,會按照建立時的順序保存常規屬性。
經過引入這兩個屬性,加速了 V8 查找屬性的速度,爲了更加進一步提高查找效率,V8 還實現了內置內屬性的策略,當常規屬性少於必定數量時,V8 就會將這些常規屬性直接寫進對象中,這樣又節省了一箇中間步驟。
可是若是對象中的屬性過多時,或者存在反覆添加或者刪除屬性的操做,那麼 V8 就會將線性的存儲模式降級爲非線性的字典存儲模式,這樣雖然下降了查找速度,可是卻提高了修改對象的屬性的速度。
資料拓展: 快屬性和慢屬性:V8 是怎樣提高對象屬性訪問速度的?
棧的優點和缺點:
雖然操做速度很是快,可是棧也是有缺點的,其中最大的缺點也是它的優勢所形成的,那就是棧是連續的,因此要想在內存中分配一塊連續的大空間是很是難的,所以棧空間是有限的。
// 棧溢出 function factorial(n) { if (n === 1) { return 1; } return n * factorial(n - 1); } console.log(factorial(50000));
繼承就是一個對象能夠訪問另一個對象中的屬性和方法,在 JavaScript 中,咱們經過原型和原型鏈的方式來實現了繼承特性。
JavaScript 的每一個對象都包含了一個隱藏屬性 __proto__
,咱們就把該隱藏屬性 __proto__
稱之爲該對象的原型 (prototype),__proto__
指向了內存中的另一個對象,咱們就把 __proto__
指向的對象稱爲該對象的原型對象,那麼該對象就能夠直接訪問其原型對象的方法或者屬性。
JavaScript 中的繼承很是簡潔,就是每一個對象都有一個原型屬性,該屬性指向了原型對象,查找屬性的時候,JavaScript 虛擬機會沿着原型一層一層向上查找,直至找到正確的屬性。
__proto__
var animal = { type: 'Default', color: 'Default', getInfo: function () { return `Type is: ${this.type},color is ${this.color}.`; }, }; var dog = { type: 'Dog', color: 'Black', };
利用__proto__
實現繼承:
dog.__proto__ = animal; dog.getInfo();
一般隱藏屬性是不能使用 JavaScript 來直接與之交互的。雖然現代瀏覽器都開了一個口子,讓 JavaScript 能夠訪問隱藏屬性 __proto__
,可是在實際項目中,咱們不該該直接經過 __proto__
來訪問或者修改該屬性,其主要緣由有兩個:
__proto__
會直接破壞現有已經優化的結構,觸發 V8 重構該對象的隱藏類!在 JavaScript 中,使用 new 加上構造函數的這種組合來建立對象和實現對象的繼承。不過使用這種方式隱含的語義過於隱晦。實際上是 JavaScript 爲了吸引 Java 程序員、在語法層面去蹭 Java 熱點,因此就被硬生生地強制加入了很是不協調的關鍵字 new。
function DogFactory(type, color) { this.type = type; this.color = color; } var dog = new DogFactory('Dog', 'Black');
其實當 V8 執行上面這段代碼時,V8 在背後悄悄地作了如下幾件事情:
var dog = {}; dog.__proto__ = DogFactory.prototype; DogFactory.call(dog, 'Dog', 'Black');
隨着移動設備的普及,V8 團隊逐漸發現將 JavaScript 源碼直接編譯成二進制代碼存在兩個致命的問題:
這兩個問題無疑會阻礙 V8 在移動設備上的普及,因而 V8 團隊大規模重構代碼,引入了中間的字節碼。字節碼的優點有以下三點:
// test.js function add(x, y) { var z = x + y; return z; } console.log(add(1, 2));
運行./d8 ./test.js --print-bytecode
:
[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)] Parameter count 3 #三個參數,包括了顯式地傳入的 x 和 y,還有一個隱式地傳入的 this Register count 1 Frame size 8 0x10008250026 @ 0 : 25 02 Ldar a1 #將a1寄存器中的值加載到累加器中,LoaD Accumulator from Register 0x10008250028 @ 2 : 34 03 00 Add a0, [0] 0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中 0x1000825002d @ 7 : aa Return #結束當前函數的執行,並將控制權傳回給調用方 Constant pool (size = 0) Handler Table (size = 0) Source Position Table (size = 0) 3
經常使用字節碼指令:
Add:Add a0, [0]
是從 a0 寄存器加載值並將其與累加器中的值相加,而後將結果再次放入累加器。
add a0 後面的[0]稱之爲 feedback vector slot,又叫 反饋向量槽,它是一個數組,解釋器將解釋執行過程當中的一些數據類型的分析信息都保存在這個反饋向量槽中了,目的是爲了給 TurboFan 優化編譯器提供優化信息,不少字節碼都會爲反饋向量槽提供運行時信息。
V8 中的字節碼指令集
JavaScript 是一門動態語言,其執行效率要低於靜態語言,V8 爲了提高 JavaScript 的執行速度,借鑑了不少靜態語言的特性,好比實現了 JIT 機制,爲了提高對象的屬性訪問速度而引入了隱藏類,爲了加速運算而引入了內聯緩存。
靜態語言中,如 C++ 在聲明一個對象以前須要定義該對象的結構,代碼在執行以前須要先被編譯,編譯的時候,每一個對象的形狀都是固定的,也就是說,在代碼的執行過程當中是沒法被改變的。能夠直接經過偏移量查詢來查詢對象的屬性值,這也就是靜態語言的執行效率高的一個緣由。
JavaScript 在運行時,對象的屬性是能夠被修改的,因此當 V8 使用了一個對象時,好比使用了 obj.x 的時候,它並不知道該對象中是否有 x,也不知道 x 相對於對象的偏移量是多少,也就是說 V8 並不知道該對象的具體的形狀。那麼,當在 JavaScript 中要查詢對象 obj 中的 x 屬性時,V8 會按照具體的規則一步一步來查詢,這個過程很是的慢且耗時。
具體地講,V8 對每一個對象作以下兩點假設:
符合這兩個假設以後,V8 就能夠對 JavaScript 中的對象作深度優化了。V8 會爲每一個對象建立一個隱藏類,對象的隱藏類中記錄了該對象一些基礎的佈局信息,包括如下兩點:
// test.js let point1 = { x: 100, y: 200 }; let point2 = { x: 200, y: 300 }; let point3 = { x: 100 }; %DebugPrint(point1); %DebugPrint(point2); %DebugPrint(point3);
./d8 --allow-natives-syntax ./test.js
# =============== DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE] # V8 爲 point1 對象建立的隱藏類 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) #y: 200 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE] # V8 爲 point2 對象建立的隱藏類 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 200 (const data field 0) #y: 300 (const data field 1) } 0x1ea308284ce9: [Map] - type: JS_OBJECT_TYPE - instance size: 20 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 # =============== DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE] # V8 爲 point3 對象建立的隱藏類 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1ea3080406e9 <FixedArray[0]> { #x: 100 (const data field 0) } 0x1ea308284d39: [Map] - type: JS_OBJECT_TYPE - instance size: 16 - inobject properties: 1 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1> - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]> - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1> - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)> - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
在 V8 中,每一個對象都有一個 map 屬性,該屬性值指向該對象的隱藏類。不過若是兩個對象的形狀是相同的,V8 就會爲其複用同一個隱藏類,這樣有兩個好處:
那麼,什麼狀況下兩個對象的形狀是相同的,要知足如下兩點:
// test.js let point = {}; %DebugPrint(point); point.x = 100; %DebugPrint(point); point.y = 200; %DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties] ... DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE] - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties] ...
最佳實踐
雖然隱藏類可以加速查找對象的速度,可是在 V8 查找對象屬性值的過程當中,依然有查找對象的隱藏類和根據隱藏類來查找對象屬性值的過程。若是一個函數中利用了對象的屬性,而且這個函數會被屢次執行:
function loadX(obj) { return obj.x; } var obj = { x: 1, y: 3 }; var obj1 = { x: 3, y: 6 }; var obj2 = { x: 3, y: 6, z: 8 }; for (var i = 0; i < 90000; i++) { loadX(obj); loadX(obj1); // 產生多態 loadX(obj2); }
一般 V8 獲取 obj.x 的流程:
內聯緩存及其原理:
單態、多態和超態:
總結:
V8 引入了內聯緩存(IC),IC 會監聽每一個函數的執行過程,並在一些關鍵的地方埋下監聽點,這些包括了加載對象屬性 (Load)、給對象屬性賦值 (Store)、還有函數調用 (Call),V8 會將監聽到的數據寫入一個稱爲反饋向量 (FeedBack Vector) 的結構中,同時 V8 會爲每一個執行的函數維護一個反饋向量。有了反饋向量緩存的臨時數據,V8 就能夠縮短對象屬性的查找路徑,從而提高執行效率。可是針對函數中的同一段代碼,若是對象的隱藏類是不一樣的,那麼反饋向量也會記錄這些不一樣的隱藏類,這就出現了多態和超態的狀況。咱們在實際項目中,要儘可能避免出現多態或者超態的狀況。
回調函數有兩種類型:同步回調和異步回調,同步回調函數是在執行函數內部被執行的,而異步回調函數是在執行函數外部被執行的。
通用 UI 線程宏觀架構:
UI 線程提供一個消息隊列,並將待執行的事件添加到消息隊列中,而後 UI 線程會不斷循環地從消息隊列中取出事件、執行事件。關於異步回調,這裏也有兩種不一樣的類型,其典型表明是 setTimeout 和 XMLHttpRequest:
微任務是基於消息隊列、事件循環、UI 主線程還有堆棧而來的,而後基於微任務,又能夠延伸出協程、Promise、Generator、await/async 等現代前端常用的一些技術。
// 不會使瀏覽器卡死 function foo() { setTimeout(foo, 0); } foo();
微任務:
// 瀏覽器console控制檯可以使瀏覽器卡死(沒法響應鼠標事件等) function foo() { return Promise.resolve().then(foo); } foo();
協程是一種比線程更加輕量級的存在。你能夠把協程當作是跑在線程上的任務,一個線程上能夠存在多個協程,可是在線程上同時只能執行一個協程。好比,當前執行的是 A 協程,要啓動 B 協程,那麼 A 協程就須要將主線程的控制權交給 B 協程,這就體如今 A 協程暫停執行,B 協程恢復執行;一樣,也能夠從 B 協程中啓動 A 協程。一般,若是從 A 協程啓動 B 協程,咱們就把 A 協程稱爲 B 協程的父協程。正如一個進程能夠擁有多個線程同樣,一個線程也能夠擁有多個協程。每一時刻,該線程只能執行其中某一個協程。最重要的是,協程不是被操做系統內核所管理,而徹底是由程序所控制(也就是在用戶態執行)。這樣帶來的好處就是性能獲得了很大的提高,不會像線程切換那樣消耗資源。
資料拓展:co 函數庫的含義和用法
從「GC Roots」對象出發,遍歷 GC Root 中的全部對象,若是經過 GC Roots 沒有遍歷到的對象,則這些對象即是垃圾數據。V8 會有專門的垃圾回收器來回收這些垃圾數據。
垃圾回收大體能夠分爲如下幾個步驟:
第一步,經過 GC Root 標記空間中活動對象和非活動對象。目前 V8 採用的可訪問性(reachability)算法來判斷堆中的對象是不是活動對象。具體地講,這個算法是將一些 GC Root 做爲初始存活的對象的集合,從 GC Roots 對象出發,遍歷 GC Root 中的全部對象:
在瀏覽器環境中,GC Root 有不少,一般包括瞭如下幾種 (可是不止於這幾種):
V8 依據代際假說,將堆內存劃分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放生存時間久的對象。代際假說有兩個特色:
爲了提高垃圾回收的效率,V8 設置了兩個垃圾回收器,主垃圾回收器和副垃圾回收器。
副垃圾回收器採用了 Scavenge 算法,是把新生代空間對半劃分爲兩個區域(有些地方也稱做From和To空間),一半是對象區域,一半是空閒區域。新的數據都分配在對象區域,等待對象區域快分配滿的時候,垃圾回收器便執行垃圾回收操做,以後將存活的對象從對象區域拷貝到空閒區域,並將兩個區域互換。
主垃圾回收器回收器主要負責老生代中的垃圾數據的回收操做,會經歷標記、清除和整理過程。
因爲 JavaScript 是運行在主線程之上的,所以,一旦執行垃圾回收算法,都須要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。咱們把這種行爲叫作全停頓(Stop-The-World)。
V8 最開始的垃圾回收器有兩個特色:
因爲這兩個緣由,很容易形成主線程卡頓,因此 V8 採用了不少優化執行效率的方案。
Daniel Clifford 在 Google I/O 2012 上作了一個精彩的演講「Breaking the JavaScript Speed Limit with V8」。在演講中,他深刻解釋了 13 個簡單的代碼優化方法,可讓你的JavaScript代碼在 Chrome V8 引擎編譯/運行時更加快速。在演講中,他介紹了怎麼優化,並解釋了緣由。下面簡明的列出了13 個 JavaScript 性能提高技巧:
try{} catch{}
(若是存在 try/catch
代碼快,則將性能敏感的代碼放到一個嵌套的函數中);演講資料參考: Performance Tips for JavaScript in V8 | 譯文 | 內網視頻 | YouTube
資料參考: How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 譯文
資料參考: JavaScript Start-up Performance | JavaScript 啓動性能瓶頸分析與解決方案