《图解 Google V8》

《图解 Google V8》专栏总结

前言

这是一门短小精悍的课程,整个专栏就 24 讲,没有一行一行的分析 Chrome V8 代码。高屋建瓴地拆解 V8 的每一块内容,那估计 100 讲也说不完,编程领域论代码复杂性,老大是操作系统,其次是浏览器,所以专栏作者主要说了 V8 的核心特性,以及从前端角度触发,将两者相结合讲解,对于入门 V8 和进阶高阶 JS 是没什么问题。

知识图谱

来自《图解 Google V8》

从五个纬度拆解 V8 的知识

  • V8 基础环境
  • V8 执行流程
  • 事件循环系统
  • 垃圾回收机制
  • JS 设计思想

V8 基础环境

  • 堆空间
  • 栈空间
  • 全局执行上下文
  • 宿主环境
  • 事件循环系统

V8 执行流程

  • 词法分析
  • 语法分析
  • 字节码
  • 及时编译

事件循环系统

  • 消息队列
  • 任务调度
  • 异步编程

垃圾回收机制

  • 回收方式
  • 代记假说
  • 垃圾回收器,主垃圾回收器和副垃圾回收器
  • 垃圾回收算法,Scavenge 算法(副),标记-清除,标记-整理(主)。
  • 垃圾回收流程

JS 设计思想

  • 函数是一等公民
  • 类型系统和自动垃圾回收
  • 作用域
  • 原型链继承

宏观视角

V8 是执行一段 JS 代码,其主核心流程是先将 JS 代码转为低级中间代码或者机器码,再执行机器代码。你可以把 V8 看成是一个虚构出来的计算机,也称为虚拟机,虚拟机通过模拟实际计算机的各种功能来实现代码的执行,如模拟实际计算机的 CPU、堆栈、寄存器等,虚拟机还具有它自己的一套指令系统。这样我们不需要操作系统,就构建自己的应用。

JS 设计思想

  • 函数即对象
  • 快属性和慢属性:V8 如何提升对象属性访问速度
  • 函数表达式
  • 原型链,V8 实现对象继承
  • 作用域链,V8 查找变量
  • 类型转换,实现 1 + “2”
  • 查看 V8 编译工具 D8

V8编译流水线

  • 运行时环境
  • 机器代码,二进制代码被CPU执行
  • 堆和栈,函数调用影响内存布局
  • 延迟解析,实现闭包
  • 字节码,引入中间字节码,体积小,缓存命中率高
  • 隐藏类,可快速查找对象属性
  • 内联缓存,提供函数执行效率

事件循环和垃圾回收

  • 消息队列,实现函数回调
  • 异步编程,实现微任务和宏任务
  • 垃圾回收,主垃圾回收器和副垃圾回收器,Scavenge 算法(副),标记-清除,标记-整理(主)

总结

Chrome V8对于很多前端同学都是黑盒,如果我们要理解每一个设计意图和设计API,无疑是自找没趣,如果能从宏观的角度,先知道V8的大体架构,以及它的演进历程,那么就会明白它的设计意图,也能结合前端的变革一同分析,这无疑是渐进式地,且增加了关联性,比较容易形成自己的理解。所以我强烈推荐这个专栏作为V8的入门和JS进阶。

 

下面是一些笔记

1、为什么父类上被继承的方法或者属性要写在原型(prototype)上,而不直接绑定到this上?

因为绑定到this上,每次new调用构造函数的时候,都会生成一份新的数据,而prototype是内存共享的。

2、v8有哪些优化手段

我们知道 JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。

3、V8隐藏类有什么作用?

静态语言,比如C++,在创建对象的时候知道对象属性的类型,即知道对象占用的空间是多少,所以也知道对象的偏移量,通过偏移量可以快速查找属性,和数组下标查找元素,时间复杂度只要O(1)是同一个道理。而V8引入隐藏类,在编译代码的时候,预解析对象的类型,并为对象设置隐藏类。但JS因为运行时可以修改对象的属性,如果动态修改了对象属性类型,很可能导致隐藏类的优化失效,我们在运行时尽量不要修改对象类型,新增或者删除属性也会使隐藏类失效。

4、V8内联缓存

其实 内联缓存(IC,inline cache) 的原理很简单,直观地理解,就是在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键的中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。

5、宏任务和微任务

宏任务是在主线程的消息队列里,一个宏任务下面可以有多个微任务。主线程有一个循环,从消息队列中读取宏任务,执行结束再执行下一个。而微任务是在当前执行的调用栈的微任务队列里,微任务其实是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。下面是一张主线程普通任务,宏任务,微任务的示意图。

图片来自《图解 Google V8》专栏

6、async和await

用来解决回调地狱。在之前用generator解决回调地狱,但是状态还需要外部手动控制,所以有心智负担。generator是JS中协程的概念,协程是比线程更轻量的存在,是在内存间执行,无像线程间切换的开销。而async像写同步代码一样写异步流程,支持try catch,接收一个普通函数或者一个promise。

7、V8垃圾回收

分为主垃圾回收和副垃圾回收。分别负责老生代和新生代垃圾回收。

副垃圾回收即频繁触发的垃圾回收,因为回收频繁,所以占用的内存空间少,一般是几M。新生代使用的是scanvenger算法,即将内存空间均分为两个空间,每次将少量数据存入一个空间,当空间满了,就进行一次GC操作,清理没有引用的对象,并且将不连续的内存变成连续的,两个空间互换,并清除最新的。如果一个对象在两次变换中还存在,即晋升到老生代。

老生代是保存大对象,以及存活时间久的对象。使用标记清除算法。先标记,然后清除,但是内存空间里的对象还是不连续,所以就引入了标记-整理(mark-compact)。标记即标记清除,整理即整理内存碎片。

主垃圾回收器同时采用了这三种策略:

首先主垃圾回收器主要使用并发标记,我们可以看到,在主线程执行 JavaScript,辅助线程就开始执行标记操作了,所以说标记是在辅助线程中完成的。

标记完成之后,再执行并行清理操作。主线程在执行清理操作时,多个辅助线程也在执行清理操作。

另外,主垃圾回收器还采用了增量标记的方式,清理的任务会穿插在各种 JavaScript 任务之间执行。

8、V8内存泄漏

闭包可能会导致内存泄漏。所以闭包内引用的对象要及时销毁或者引用少量数据。

9、V8内存回收

如果频繁使用大的临时变量,那么就会导致频繁垃圾回收,频繁的垃圾回收操作会让你感觉到页面卡顿,要解决这个问题,我们可以考虑将这些临时变量设置为全局变量。


V8是如何执行JavaScript代码的?

ziv
ziv
乘风破浪

V8是如何执行JavaScript代码的?

编程语言是如何运行的

众所周知,我们通过编程语言完成的程序是通过处理器运行的。但是处理器不能直接理解我们通过高级语言(如C++、Go、JavaScript等)编写的代码,只能理解机器码,所以在执行程序之前,需要经过一系列的步骤,将我们编写的代码翻译成机器语言。这个过程一般是由编译器(Compiler) 或者解释器(Interpreter) 来完成。

那么编译器和解释器的工作流程是怎样的呢?

 

从上图可以看出它们的大概的工作流程。那么既然编译器和解释器都可以完成代码翻译的工作,为何还同时存在呢?

这是因为编程语言有两个类别:静态类型和动态类型。静态类型的语言,比如C++、Go等,都需要提前编译 (AOT) 成机器码然后执行,这个过程主要使用编译器来完成;而动态语言,比如JavaScript、Python等,只在运行时进行编译执行 (JIT) ,这个过程通过解释器完成。

通过上面的描述,我们已经知道了JavaScript是通过解释器来进行翻译执行的,那么JavaScript引擎V8执行Js代码的详细过程是怎么样的呢?接下来我们详细分析一下。

V8执行Js代码的过程

V8执行Js代码的整体流程如下图所示:

在这个过程中,V8同时使用了Parser(解析器)、Ignition(解释器) 和TurboFan(编译器) 来执行Js代码。

1.Parser生成抽象语法树

在Chrome中开始下载Javascript文件后,Parser就会开始并行在单独的线程上解析代码。这意味着解析可以在下载完成后仅几毫秒内完成,并生成AST。

 

上图是一段Js代码转成AST后的结构图,从图中可以看出AST是把代码结构化成树状结构表示,这样做是为了更好的让编译器或者解释器理解。此外,AST还广泛应用于各类项目中,比如Babel、ESLint,那么AST的生成过程是怎么样的呢?

1. 词法分析(lexical analysis):主要是将字符流(char stream) 转换成标记流(token stream),字符流就是我们一行一行的代码,token是指语法上不能再分的、最小的单个字符或者字符串。

 

var name = "ivweb"
//转成token后为

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "name"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "String",
        "value": "\"ivweb\""
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

从上面可以看出,var name = "ivweb"; 这样一段代码,会有关键字"var"、标识符"name"、赋值运算符"="、字符串"ivweb"、分隔符";",共5个token。
2. 语法分析:将前面生成的token流根据语法规则,形成一个有元素层级嵌套的语法规则树,这个树就是AST。在此过程中,如果源代码不符合语法规则,则会终止,并抛出“语法错误”。

2.Ignition生成字节码

 

字节码是机器码的抽象,可以看作是小型的构建块,这些构建块组合到一起构成任何JavaScript功能。字节码比机器码占用更小的内存,这也是为什么V8使用字节码的一个很重要的原因。字节码不能够直接在处理器上运行,需要通过解释器将其转换为机器码后才能执行。

 

通过上图可以看出,Ignition把前一步得到的AST通过字节码生成器经过一些列的优化生成字节码。
在这个过程中:

  • Register Optimizer: 主要是避免寄存器不必要的加载和存储;
  • Peephole Optimizer: 寻找直接码中可以复用的部分,并进行合并;
  • Dead-code Elimination: 删除无用的代码,减少字节码的大小

通过上面三个过程的优化进一步减小字节码的大小并提高性能,最后Ignition执行优化后的字节码。

3.执行代码及优化

 

Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan,然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。
另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition。


极客时间 | 一文帮你彻底搞懂JavaScript的函数特点

 
作者 | 李兵
来源 | 极客时间 《图解 Google V8》

你好,我是李兵。这是我们专栏的第二讲,我们来看下“函数是一等公民”背后的含义。

如果你熟悉了一门其他流行语言,再来使用 JavaScript,那么 JavaScript 中的函数可能会给你造成一些误解,比如在 JavaScript 中,你可以将一个函数赋值给一个变量,还可以将函数作为一个参数传递给另外一个函数,甚至可以使得一个函数返回另外一个函数,这在一些主流语言中都很难实现。

JavaScript 中的函数非常灵活,其根本原因在于 JavaScript 中的函数就是一种特殊的对象,我们把 JavaScript 中的函数称为一等公民 (First Class Function)。

基于函数是一等公民的设计,使得 JavaScript 非常容易实现一些特性,比如闭包,还有函数式编程等,而其他语言要实现这些特性就显得比较困难,比如要在 C++ 中实现闭包需要实现大量复杂的代码,而且使用起来也异常复杂。

函数式编程和闭包在实际的项目中会经常遇到,如果不了解这些特性,那么在你使用第三方代码时就会非常吃力,同时自己也很难使用这些特性写出优雅的代码,因此我们很有必要了解这些特性的底层机制。

另外,在我们后续课程介绍 V8 工作机制时,会学习 V8 是怎么实现闭包的,还会学习 V8 是如何将 JavaScript 的动态特性静态化以加快代码的执行速度,这些内容都涉及到 JavaScript 中的函数底层特性。

今天,我们就来深入分析下,JavaScript 中的“函数”到底有怎样的特点。

什么是 JavaScript 中的对象?

既然在 JavaScript 中,函数就是一种特殊的对象,那我们首先要明白,什么是 JavaScript 中的“对象”?它和面向对象语言中的“对象”有什么区别?

和其他主流语言不一样的是,JavaScript 是一门基于对象 (Object-Based) 的语言,可以说 JavaScript 中大部分的内容都是由对象构成的,诸如函数、数组,也可以说 JavaScript 是建立在对象之上的语言。

而这些对象在运行时可以动态修改其内容,这造就了 JavaScript 的超级灵活特性。不过,因为 JavaScript 太灵活了,也加大了理解和使用这门语言的难度。

虽然 JavaScript 是基于对象设计的,但是它却不是一门面向对象的语言 (Object-Oriented Programming Language),因为面向对象语言天生支持封装、继承、多态,但是 JavaScript 并没有直接提供多态的支持,因此要在 JavaScript 中使用多态并不是一件容易的事。

除了对多态支持的不好,JavaScript 实现继承的方式和面向对象的语言实现继承的方式同样存在很大的差异。

面向对象语言是由语言本身对继承做了充分的支持,并提供了大量的关键字,如 public、protected、friend、interface 等,众多的关键字使得面向对象语言的继承变得异常繁琐和复杂,而 JavaScript 中实现继承的方式却非常简单清爽,只是在对象中添加了一个称为原型的属性,把继承的对象通过原型链接起来,就实现了继承,我们把这种继承方式称为基于原型链继承。关于 V8 是如何支持原型的,我们会在《05 | 原型链:V8 是如何实现对象继承的?》这节课做具体介绍。

既然“JavaScript 中的对象”和“面向对象语言中的对象”存在巨大差异,那么在 JavaScript 中,我们所谈论的对象到底是指什么呢?

其实 JavaScript 中的对象非常简单,每个对象就是由一组组属性和值构成的集合,比如我使用下面代码创建了一个 person 对象:

var person=new Object();
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";

这个对象里面有四个属性,为了直观理解,你可以参看下图:

上图展示了对象 person 的结构,我们可以看到蓝色的属性在左边,黄色的值在右边,有多组属性和值组成,这就是 JavaScript 中的对象,虽然 JavaScript 对象用途非常广泛,使用的方式也非常之多,但是万变不离其宗,其核心本质都就是由一组组属性和值组成的集合,抓住了这一点,当我们再分析对象时,就会轻松很多。

在 JavaScript 中,之所以它的对象的用途这么广,是因为对象的值可以是任意类型的数据,我们可以改造下上面的那段代码,来看看对象的值都有那些类型?改造后的代码如下所示:

var person=new Object()
person.firstname="John"
person.lastname="Doe"
person.info = new Object()
person.info.age=50
person.info.eyecolor="blue"
person.showinfo = function (){
    console.log(/*...*/)
}

我们可以先画出这段代码的内存布局,如下图所示:

观察上图,我们可以看出来,对象的属性值有三种类型:

第一种是原始类型 (primitive),所谓的原始类的数据,是指值本身无法被改变,比如 JavaScript 中的字符串就是原始类型,如果你修改了 JavaScript 中字符串的值,那么 V8 会返回给你一个新的字符串,原始字符串并没有被改变,我们称这些类型的值为“原始值”。

JavaScript 中的原始值主要包括 null、undefined、boolean、number、string、bigint、symbol 这七种。

第二种就是我们现在介绍的对象类型 (Object),对象的属性值也可以是另外一个对象,比如上图中的 info 属性值就是一个对象。

第三种是函数类型 (Function),如果对象中的属性值是函数,那么我们把这个属性称为方法,所以我们又说对象具备属性和方法,那么上图中的 showinfo 就是 person 对象的一个方法。

函数的本质

分析完对象,现在我们就能更好地理解 JavaScript 中函数的概念了。

在这节课开始我就提到,在 JavaScript 中,函数是一种特殊的对象,它和对象一样可以拥有属性和值,但是函数和普通对象不同的是,函数可以被调用。

我们先来看一段 JavaScript 代码,在这段代码中,我们定义了一个函数 foo,接下来我们给 foo 函数设置了 myName 和 uName 的属性。

function foo(){
    var test = 1
}
foo.myName = 1
foo.uName = 2
console.log(foo.myName)

既然是函数,那么它也可以被调用。比如你定义了一个函数,便可以通过函数名称加小括号来实现函数的调用,代码如下所示:

function foo(){
    var test = 1
    console.log(test)
}
foo()

除了使用函数名称来实现函数的调用,还可以直接调用一个匿名函数,代码如下所示:

(function (){
    var test = 1
    console.log(test)
})()

那么,V8 内部是怎么实现函数可调用特性的呢?

其实在 V8 内部,我们会为函数对象添加了两个隐藏属性,具体属性如下图所示:

也就是说,函数除了可以拥有常用类型的属性值之外,还拥有两个隐藏属性,分别是 name 属性和 code 属性。

隐藏 name 属性的值就是函数名称,如果某个函数没有设置函数名,如下面这段函数:

(function (){
    var test = 1
    console.log(test)
})()

该函数对象的默认的 name 属性值就是 anonymous,表示该函数对象没有被设置名称。另外一个隐藏属性是 code 属性,其值表示函数代码,以字符串的形式存储在内存中。当执行到一个函数调用语句时,V8 便会从函数对象中取出 code 属性值,也就是函数代码,然后再解释执行这段函数代码。

函数是一等公民

因为函数是一种特殊的对象,所以在 JavaScript 中,函数可以赋值给一个变量,也可以作为函数的参数,还可以作为函数的返回值。如果某个编程语言的函数可以和它的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。支持函数是一等公民的语言可以使得代码逻辑更加清晰,代码更加简洁。

但是由于函数的“可被调用”的特性,使得实现函数的可赋值、可传参和可作为返回值等特性变得有一点麻烦。为什么?

我们知道,在执行 JavaScript 函数的过程中,为了实现变量的查找,V8 会为其维护一个作用域链,如果函数中使用了某个变量,但是在函数内部又没有定义该变量,那么函数就会沿着作用域链去外部的作用域中查找该变量,具体流程如下图所示:

从图中可以看出,当函数内部引用了外部的变量时,使用这个函数进行赋值、传参或作为返回值,你还需要保证这些被引用的外部变量是确定存在的,这就是让函数作为一等公民麻烦的地方,因为虚拟机还需要处理函数引用的外部变量。我们来看一段简单的代码:

function foo(){
    var number = 1
    function bar(){
        number++
        console.log(number)
    }
    return bar
}
var mybar = foo()
mybar()

观察上段代码可以看到,我们在 foo 函数中定义了一个新的 bar 函数,并且 bar 函数引用了 foo 函数中的变量 number,当调用 foo 函数的时候,它会返回 bar 函数。

那么所谓的“函数是一等公民”就体现在,如果要返回函数 bar 给外部,那么即便 foo 函数执行结束了,其内部定义的 number 变量也不能被销毁,因为 bar 函数依然引用了该变量。

我们也把这种将外部变量和和函数绑定起来的技术称为闭包。V8 在实现闭包的特性时也做了大量的额外的工作,关于闭包的详细实现,我们会在《12 | 延迟解析:V8 是如何实现闭包的?》这节课再介绍。

另外基于函数是一等公民,我们可以轻松使用 JavaScript 来实现目前比较流行的函数式编程,函数式编程规则很少,非常优美,不过这并不是本专栏的重点,所以我们先略开不讲。

总结

好了,今天的内容就介绍到这里,下面我来总结下本文的主要内容。

本文我们围绕 JavaScript 中的函数来展开介绍,JavaScript 中的函数非常灵活,既可以被调用,还可以作为变量、参数和返回值,这些特性使得函数的用法非常多,这也导致了函数变得有些复杂,因此本文的目的就是要讲清楚函数到底是什么?

因为函数是一种特殊的对象,所以我们先介绍了 JavaScript 中的对象,JavaScript 中的对象就是由一组一组属性和值组成的集合,既然函数也是对象,那么函数也是由一组组值和属性组成的集合,我们还在文中使用了一段代码证明了这点。

因为函数作为一个对象,是可以被赋值、作为参数,还可以作为返回值的,那么如果一个函数返回了另外一个函数,那么就应该返回该函数所有相关的内容。

接下来,我们又介绍了一个函数到底关联了哪些内容:

  • 函数作为一个对象,它有自己的属性和值,所以函数关联了基础的属性和值;
  • 函数之所以成为特殊的对象,这个特殊的地方是函数可以“被调用”,所以一个函数被调用时,它还需要关联相关的执行上下文。

结合以上两点,JavaScript 中的函数就实现了“函数是一等公民”的特性。

思考题

本文我们从对象聊到了闭包,那么留给你的问题是,哪些语言天生支持“函数是一等公民”?


猜你喜欢

转载自www.cnblogs.com/cx2016/p/13208062.html
v8