V8 是 JavaScript 虚拟机的一种。我们可以简单地把 JavaScript 虚拟机理解成是一个翻译程序,将人类能够理解的编程语言 JavaScript,翻译成机器能够理解的机器语言
V8 引入JIT, 改变了js解释型编译
JIT是一种解释和编译混合语言
3
js 函数是一种特殊的对象,有两种隐藏属性,name and code
该函数对象的默认的 name 属性值就是 anonymous,表示该函数对象没有被设置名称。另外一个隐藏属性是 code 属性,其值表示函数代码,以字符串的形式存储在内存中。当执行到一个函数调用语句时,V8 便会从函数对象中取出 code 属性值,也就是函数代码,然后再解释执行这段函数代码。**
之所以出现这样的结果,是因为在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。
排序属性和常规属性
为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。
不建议 delete 可能会影响性能的地方: 1.如果删除排序属性,线性存储结构会有个O(n)复杂度的移动。 2.如果删除常规属性,可能会重新计算并添加快属性。
变量提升。
立即函数表达式
作用 一是不必为函数命名,避免了污染全局变量 二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
// This doesn't work like you might think, because the value of `i` never
// gets locked in. Instead, every link, when clicked (well after the loop
// has finished executing), alerts the total number of elements, because
// that's what the value of `i` actually is at that point.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + i );
}, 'false' );
}
// This works, because inside the IIFE, the value of `i` is locked in as
// `lockedInIndex`. After the loop has finished executing, even though the
// value of `i` is the total number of elements, inside the IIFE the value
// of `lockedInIndex` is whatever the value passed into it (`i`) was when
// the function expression was invoked, so when a link is clicked, the
// correct value is alerted.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
(function( lockedInIndex ){
elems[ i ].addEventListener( 'click', function(e){
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
}, 'false' );
})( i );
}
// You could also use an IIFE like this, encompassing (and returning) only
// the click handler function, and not the entire `addEventListener`
// assignment. Either way, while both examples lock in the value using an
// IIFE, I find the previous example to be more readable.
var elems = document.getElementsByTagName( 'a' );
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', (function( lockedInIndex ){
return function(e){
e.preventDefault();
alert( 'I am link #' + lockedInIndex );
};
})( i ), 'false' );
}
(function($){
//插件实现代码
})(jQuery);
这里的jquery其实是该匿名函数的参数,联想一下我们调用匿名函数时候是用f()那么匿名带参数的就是f(args)对吧,这里把jquery作为参数传入该函数,那么在函数内部使用形参这个限定符,你在这个函数内部可以随意折腾
继承就是一个对象可以访问另外一个对象中的属性和方法,在JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。
6
作用域链:
全局作用域是在 V8 启动过程中就创建了,且一直保存在内存中不会被销毁的,直至 V8 退出。 而函数作用域是在执行该函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了。
var name = '极客时间'
var type = 'global'
function foo(){
var name = 'foo'
console.log(name)
console.log(type)
}
function bar(){
var name = 'bar'
var type = 'function'
foo()
}
bar()
var name = '极客时间'
var type = 'global'
function bar() {
var type = 'function'
function foo() {
console.log(type)
}
foo()
}
bar()
沿着 foo 函数作用域–>bar 函数作用域–> 全局作用域 ;还是,沿着 foo 函数作用域—> 全局作用域?因为 JavaScript 是基于词法作用域的,词法作用域就是指,查找作用域的顺序是按照函数定义时的位置来决定的。bar 和 foo 函数的外部代码都是全局代码,所以无论你是在 bar 函数中查找变量,还是在 foo 函数中查找变量,其查找顺序都是按照当前函数作用域–> 全局作用域这个路径来的。由于我们代码中的 foo 函数和 bar 函数都是在全局下面定义的,所以在 foo 函数中使用了 type,最终打印出来的值就是全局作用域中的 type。
var name = '极客时间'
var type = 'global'
function bar() {
var type = 'function'
function foo() {
console.log(this.type)
}
foo()
}
bar()
上下文包含了作用域、变量对象、this等,作用域只用来确定变量/函数的可访问范围。
var a = [];
for(let i = 0;i<10;i++){
a[i]=function(){
console.log(i)
}
};
a[2]();
作者回复: let定义的i会运行for的块级作用域中,每次执行一次循环,都会创建一个块级作用域。
在这个块级作用域中,你又定义了一个函数,而这个函数又引用了函数外部的i变量,那么这就产生了闭包,也就是说,所有块级作用域中的i都不会被销毁,你在这里执行了10次循环,那么也就创建了10个块级作用域,这十个块级作用域中的变量i都会被保存在内存中。
那么当你再次调用该an时,v8就会拿出闭包中的变量i,并将其打印出来,因为每个闭包中的i值都不同,所以an时,打印出来的值就是n,这个就非常符合直觉了。
但是如果你将for循环中的i变量声明改成var,那么并不会产生块级作用域,那么函数引用的i就是全局作用域中的了,由于全局作用域中只有一个,那么在执行for循环的时候,i的值会一直被改变,最后是10,所以最终你执行an时,无论n是多少,打印出来的都是10. 那么这就是bug之源了。
v8编译基本确定作用域 正常情况下是这样的,单是执行eval的情况,这个eval方法很有破坏性,因为在执行eval之前,引擎并不知道eval要执行的内容,也就没有办法提前做预解析
7
类型转换
对象转换
var Obj = {
toString() {
return "200"
},
valueOf() {
return 100
}
}
Obj+"3"
9
V8 与 浏览器关系
当 V8 开始执行一段可执行代码时,会生成一个执行上下文。V8 用执行上下文来维护执行当前代码所需要的变量声明、this 指向等。执行上下文中主要包含三部分,变量环境、词法环境和 this 关键字。
这段代码使用了一个循环,不同地获取新的任务,一旦有新的任务,便立即执行该任务。如果主线程正在执行一个任务,这时候又来了一个新任务,比如 V8 正在操作 DOM,这时候浏览器的网络线程完成了一个页面下载的任务,而且 V8 注册监听下载完成的事件,那么这种情况下就需要引入一个消息队列,让下载完成的事件暂存到消息队列中,等当前的任务执行结束之后,再从消息队列中取出正在排队的任务。当执行完一个任务之后,我们的事件循环系统会重复这个过程,继续从消息队列中取出并执行下个任务。有一点你需要注意一下,因为所有的任务都是运行在主线程的,在浏览器的页面中,V8 会和页面共用主线程,共用消息队列,所以如果 V8 执行一个函数过久,会影响到浏览器页面的交互性能。
11
直接爆栈
function foo() {
foo() // 是否存在堆栈溢出错误?
}
foo()
正常
function foo() {
setTimeout(foo, 0) // 是否存在堆栈溢出错误?
}
页面卡死, 原因参考19
function foo() {
return Promise.resolve().then(foo)
}
foo()
这相当于在当前这一轮任务里不停地创建微任务,执行,创建,执行,创建……虽然不会爆栈,但也无法去执行下一个任务,主线程被卡在这里了,所以页面会卡死
12
惰性解析
,所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。利用惰性解析可以加速 JavaScript 代码的启动速度,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间。由于 JavaScript 是一门天生支持闭包的语言,由于闭包会引用当前函数作用域之外的变量,所以当 V8 解析一个函数的时候,还需要判断该函数的内部函数是否引用了当前函数内部声明的变量,如果引用了,那么需要将该变量存放到堆中,即便当前函数执行结束之后,也不会释放该变量。
function foo() {
var a = 0
}
function foo() {
var a = 0
return function inner() {
return a++
}
}
第一个函数: 变量a在栈上,当解析foo函数的时候,预解析没有发现内部函数,所有变量 a 就只会在栈上,当foo函数的执行上下文销毁的时候 a变量也会销毁;
第二个函数,变量a同时在栈和堆上,当解析foo函数的时候,预解析有发现内部函数引用外部变量 a , 这时候就会把 a 复制 到堆上,当父函数执行到 a 的赋值语句时,会同时修改 栈和堆上的变量a的值, 父函数销毁的时候也只会销毁栈上的变量a,堆上的变量 a 保留。 最后当内部函数执行完后,堆上的变量a就没有再被引用,就会被垃圾回收掉
13
我们知道 V8 在执行一段 JavaScript 代码之前,需要将其编译为字节码,然后再解释执行字节码或者将字节码编译为二进制代码然后再执行。所谓字节码,是指编译过程中的中间代码,你可以把字节码看成是机器代码的抽象,在 V8 中,字节码有两个作用:第一个是解释器可以直接解释执行字节码 ;第二个是优化编译器可以将字节码编译为二进制代码,然后再执行二进制机器代码
优点:
- 可以减少代码缓存所需空间
- 方便编译成机器代码
这节课我们介绍了 V8 为什么要引入字节码。早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制的机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。不过随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:时间问题:编译时间过久,影响代码启动速度;空间问题:缓存编译后的二进制代码占用更多的内存。这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:解决启动问题:生成字节码的时间很短;解决空间问题:字节码占用内存不多,缓存字节码会大大降低内存的使用;代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易
14
解释器直接执行中间代码(字节码)
15
隐藏类
这节课我们介绍了 V8 中隐藏类的工作机制,我们先分析了 V8 引入隐藏类的动机。因为 JavaScript 是一门动态语言,对象属性在执行过程中是可以被修改的,这就导致了在运行时,V8 无法知道对象的完整形状,那么当查找对象中的属性时,V8 就需要经过一系列复杂的步骤才能获取到对象属性。为了加速查找对象属性的速度,V8 在背后为每个对象提供了一个隐藏类,隐藏类描述了该对象的具体形状。有了隐藏类,V8 就可以根据隐藏类中描述的偏移地址获取对应的属性值,这样就省去了复杂的查找流程。不过隐藏类是建立在两个假设基础之上的:对象创建好了之后就不会添加新的属性;对象创建好了之后也不会删除属性。一旦对象的形状发生了改变,这意味着 V8 需要为对象重建新的隐藏类,这就会带来效率问题。为了避免一些不必要的性能问题,我们在程序中尽量不要随意改变对象的形状。我在这节课中也给你列举了几个最佳实践的策略。最后,关于隐藏类,我们记住以下几点。在 V8 中,每个对象都有一个隐藏类,隐藏类在 V8 中又被称为 map。在 V8 中,每个对象的第一个属性的指针都指向其 map 地址。map 描述了其对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少?如果添加新的属性,那么需要重新构建隐藏类。如果删除了对象中的某个属性,通用也需要构建隐藏类。
16
内联缓存
改变对象结构生成多个插槽
现在我们知道了,一个反馈向量的一个插槽中可以包含多个隐藏类的信息,那么:如果一个插槽中只包含 1 个隐藏类,那么我们称这种状态为单态 (monomorphic);如果一个插槽中包含了 2~4 个隐藏类,那我们称这种状态为多态 (polymorphic);如果一个插槽中超过 4 个隐藏类,那我们称这种状态为超态 (magamorphic)。
尽量保持单态这就是 IC 的一些基础情况,非常简单,只是为每个函数添加了一个缓存,当第一次执行该函数时,V8 会将函数中的存储、加载和调用相关的中间结果保存到反馈向量中。当再次执行时,V8 就要去反馈向量中查找相关中间信息,如果命中了,那么就直接使用中间信息。了解了 IC 的基础执行原理,我们就能理解一些最佳实践背后的道理,这样你并不需要去刻意记住这些最佳实践了,因为你已经从内部理解了它。总的来说,我们只需要记住一条就足够了,那就是单态的性能优于多态和超态,所以我们需要稍微避免多态和超态的情况。
function loadX(o) {
return o.x
}
var o = { x: 1,y:3}
var o1 = { x: 3 ,y:6}
for (var i = 0; i < 90000; i++) {
loadX(o)
loadX(o1)
}
这节课我们通过分析 IC 的工作原理,来介绍了它是如何提升代码执行速度的。虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行,那么 V8 就会考虑,怎么将这个查找过程再度简化,最好能将属性的查找过程能一步到位。因此,V8 引入了 IC,IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况。最后我还想强调一点,虽然我们分析的隐藏类和 IC 能提升代码的执行速度,但是在实际的项目中,影响执行性能的因素非常多,找出那些影响性能瓶颈才是至关重要的,你不需要过度关注微优化,你也不需要过度担忧你的代码是否破坏了隐藏类或者 IC 的机制,因为相对于其他的性能瓶颈,它们对效率的影响可能是微不足道的。
18
宏任务 与 微任务
基于这套基础 UI 框架,JavaScript 又延伸出很多新的技术,其中应用最广泛的当属宏任务和微任务。宏任务很简单,就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。微任务稍微复杂一点,其实你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,那么微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
微任务详情:
通俗地理解,V8 会为每个宏任务维护一个微任务队列。当 V8 执行一段 JavaScript 时,会为这段代码创建一个环境对象,微任务队列就是存放在该环境对象中的。当你通过 Promise.resolve 生成一个微任务,该微任务会被 V8 自动添加进微任务队列,等整段代码快要执行结束时,该环境对象也随之被销毁,但是在销毁之前,V8 会先处理微任务队列中的微任务。理解微任务的执行时机,你只需要记住以下两点:首先,如果当前的任务中产生了一个微任务,通过 Promise.resolve() 或者 Promise.reject() 都会触发微任务,触发的微任务不会在当前的函数中被执行,所以执行微任务时,不会导致栈的无限扩张;其次,和异步调用不同,微任务依然会在当前任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。
可以把微任务 看出在调用栈里
浏览器中的 MutationObserver 接口提供了监视对 DOM 树所做更改的能力,它在内部也使用了微任务的技术,那么今天留给你的作业是,查找 MutationObserver 相关资料,分析它是如何工作的,其中微任务的作用是什么?
垃圾回收:
垃圾回收的常见方法:
计数回收和标记回收:
目前 V8 采用了两个垃圾回收器,主垃圾回收器 -Major GC 和副垃圾回收器 -Minor GC (Scavenger)。
所以,在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。新生代通常只支持 1~8M 的容量,而老生代支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。副垃圾回收器 -Minor GC (Scavenger),主要负责新生代的垃圾回收。主垃圾回收器 -Major GC,主要负责老生代的垃圾回收。
老生代内存清理
今天,我们先分析了什么是垃圾数据,从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。主垃圾回收器负责收集老生代中的垃圾数据,副垃圾回收器负责收集新生代中的垃圾数据。副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程。
v8 垃圾回收优化的三种方式:
第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。
8
我们先看内存泄漏。本质上,内存泄漏可以定义为:当进程不再需要某些内存的时候,这些不再被需要的内存依然没有被进程回收。在 JavaScript 中,造成内存泄漏 (Memory leak) 的主要原因是不再需要 (没有作用) 的内存数据依然被其他对象引用着。
常见情景: 引用dom, 闭包,定时器, 全局变量
内存膨胀 (Memory bloat),它会导致页面的性能会一直很差;
内存膨胀和内存泄漏有一些差异,内存膨胀主要表现在程序员对内存管理的不科学,比如只需要 50M 内存就可以搞定的,有些程序员却花费了 500M 内存。额外使用过多的内存有可能是没有充分地利用好缓存,也有可能加载了一些不必要的资源。通常表现为内存在某一段时间内快速增长,然后达到一个平稳的峰值继续运行。
频繁垃圾回收,它会导致页面出现延迟或者经常暂停。
除了内存泄漏和内存膨胀,还有另外一类内存问题,那就是频繁使用大的临时变量,导致了新生代空间很快被装满,从而频繁触发垃圾回收。频繁的垃圾回收操作会让你感觉到页面卡顿
weakset weakmap set map