JavaScript学习笔记01-理解运行原理

理解浏览器执行js过程,内核实质 ,v8引擎原理

1. JavaScript的重要性

  • JavaScript是前端万丈高楼的根基

    前端行业在近几年快速发展,并且开发模式,框架越来越丰富。但是不管学习的是 Vue,React,Angular包括 jQuery,以及一些新出的框架。他们本身都是基于JavaScript的。使用他们的过程中都必须好好掌握JavaScript。所以JavaScript是前端万丈高楼的根基,无论是前端发展的万丈高楼,还是筑建自己的万丈高楼

  • JavaScript在工作中至关重要

    在工作中无论使用什么技术,比如Vue,React,Angular,uniapp,taro,ReactNatice。也无论做什么平台的应用程序,比如 pc Web,移动端web,小程序,公众号,移动端App。他们都离不开JavaScript,并且深入掌握JavaScript不仅可以提高开发效率,也可以快速解决在开发中遇到的各种问题

    面试时,往往会考察JavaScript的功底

  • 前端的未来依然是JavaScript

    在可预见的未来中,依然离不开JavaScript。目前前端快速发展,无论是框架还是构建工具,都像雨后春笋一样,琳琅满目。而且框架也会进行不断的更新,比如 Vue3,react18,vite2,TypeScript4.x。前端开发者面对这些不断变化的内容,往往内心会有很多的焦虑,但是其实只要深入掌握了JavaScript,这些框架或者工具都是离不开JavaScript的

2. Atwood定律

Stack Overflow的创立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood定律:

  • Any application that can be written in JavaScript, will eventually be written in JavaScript.

    任何可以使用JavaScript来实现的应用都最终都会使用JavaScript实现

3. JavaScript应用

3.1. web开发

  • 原生JavaScript
  • React开发
  • Vue开发
  • Angular开发

3.2. 移动端开发

  • ReactNative
  • Weex

3.3. 小程序端开发

  • 微信小程序
  • 支付宝小程序
  • uniapp
  • taro

3.4. 桌面应用开发

  • Electron

    VSCode就是Electron开发的

3.5. 后端开发

  • Node环境
    • express,koa,egg.js

4. 历史

  • Brendan Eich,1961~
  • 在1995年利用10天完成
  • 网景公司最初命名为LiveScript,后来在与Sun合作之后将其改名为JavaScript

5. 概念

  • JavaScript是一门高级的编程语言
    • 计算机本身只认识0/1的机器语言
    • 编程语言作为高级语言,需要转换成机器指令才能在计算机上运行
  • JavaScript是一种运行在客户端的脚本语言
    • 脚本语言:不需要编译,运行过程中由js引擎逐行来进行解释并执行
  • JavaScript可以基于node.js进行服务器编程

6. 浏览器执行js过程

渲染引擎+js引擎

  • 渲染引擎:用来解析html+css,俗称内核,比如chrome浏览器的blink,老版本的Webkit
  • js引擎:用来读取网页中的JavaScript代码,对其处理后运行

浏览器本身并不会执行js代码,而是通过内置JavaScript引擎来执行js代码。

js引擎执行代码时逐行解释每一句源码,将其转换为机器语言,然后由计算机去执行,所以JavaScript语言归为脚本语言,会逐行解释执行

6.1. 不同浏览器不同内核

  • Gecko:早期被Netscape和Mozilla Firefox浏览器使用

  • Trident:微软开发,被IE4~IE10浏览器使用,但是Edge浏览器已经转向Blink

  • Webkit:苹果基于KHTML开发,开源的,用于Safari,Google Chrome之前也在使用

  • Blink:webkit的分支,Google开发的,目前应用于Google Chrome,Edge,Opera等

常说的浏览器内核指的是浏览器的排版引擎(layout engine),也称为浏览器引擎(browser engine),页面渲染引擎(rendering engine),样板引擎

6.2. 渲染引擎工作过程

  • DOM Tree:在执行过程中,HTML解析的时候遇到script标签,会停止解析HTML,而去加载和执行JavaScript代码(比如document.createElement(),js可以直接创建元素)

  • Layout:显示尺寸不同导致元素布局不同

为什么不直接异步加载js代码?

JavaScript代码可以操作网页DOM,浏览器希望把HTML解析的DOM和JavaScript操作之后的DOM放到一起最终生成DOM树,而不是频繁生成新的DOM树

6.3. 为什么需要js引擎

  • 高级的编程语言都是需要转成最终的机器指令来执行的,而js就是高级编程语言;
  • 事实上编写的JavaScript代码无论是交给浏览器或者Node执行,最后都是需要被CPU执行的;
  • 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;
  • 所以需要JavaScript引擎帮忙将JavaScript代码翻译成CPU指令来执行;

6.4. 常见js引擎

  • SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者);
  • Chakra:微软开发,用于IT浏览器;
  • JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;
  • V8⭐️:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出;

7. webkit内核

以WebKit内核为例,WebKit事实上由两部分组成的:

  • WebCore:负责HTML解析、布局、渲染等等相关的工作;
  • JavaScriptCore:解析、执行JavaScript代码;

类似的在小程序中编写的JavaScript代码就是被JSCore执行的;

8. V8引擎

官方对V8引擎的定义:

  • V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等
  • 它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行
  • V8可以独立运行,也可以嵌入到任何C ++应用程序中
  • 源码 https://github.com/v8/v8/tree/master/src

8.1. V8引擎的原理

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,但是可以简单了解一下它执行JavaScript代码的原理

8.1.1. Parse

Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;

  • 如果函数没有被调用,那么是不会被转换成AST的;

  • Parse的V8官方文档:https://v8.dev/blog/scanner

  • 包括 词法分析语法分析

    https://astexplorer.net/ 中输入 const name="Hello",得到词法分析树

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    {
    "type": "Program",
    "start": 0,
    "end": 20,
    "body": [
    {
    "type": "VariableDeclaration",
    "start": 0,
    "end": 20,
    "declarations": [
    {
    "type": "VariableDeclarator",
    "start": 6,
    "end": 20,
    "id": {
    "type": "Identifier",
    "start": 6,
    "end": 10,
    "name": "name"
    },
    "init": {
    "type": "Literal",
    "start": 13,
    "end": 20,
    "value": "Hello",
    "raw": "\"Hello\""
    }
    }
    ],
    "kind": "const"
    }
    ],
    "sourceType": "module"
    }

8.1.2. Ignition

Ignition是一个解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • 如果函数只调用一次,Ignition会执行解释执行ByteCode;
  • 根据不同的平台生成不同的字节码,而生成的字节码是V8引擎约定好的,是跨平台
  • Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter

8.1.3. TurboFan

TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
  • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
  • TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit

8.1.4. 内存回收

上面是JavaScript代码的执行过程,事实上V8的内存回收也是其强大的另外一个原因:

8.2. js源码是如何被解析的

v8引擎官方解析图

JavaScript源码是如何被解析(Parse)的?

  • Blink内核(Webkit内核分支)遇到script标签时,会下载js源码,并放入数据流(Stream)中传递给v8引擎,Stream会对数据进行编码转换
  • Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens
  • 接下来tokens会被转换成AST树,经过Parser和PreParser:
    • Parser就是直接将token转成AST树架构
    • PreParse 称为预解析,为什么需要预解析?
      • 这是因为并不是所有的JavaScript代码,在一开始就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率
      • 所以v8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析在函数被调用时才会进行
      • 比如在一个函数outer内部定义了另一个函数inner,那么inner函数就会进行预解析

8.3. js代码是怎么执行的

1
2
3
4
5
6
7
8
9
10
11
12
var name = 'test'

function foo(){
var name = 'foo'
console.log(name)
}

var num1 = 20
var num2 = 30
var result = num1 + num2

console.log(result)

8.3.1. 初始化全局对象

  • v8引擎会在解析源代码到AST的过程中,在堆内存中创建一个全局对象: GlobalObject(GO)

    • 该对象所有作用域(scope)都可以访问

    • 这个对象会包含浏览器或者node环境下的全局对象:Date,Array,String,Number,setTimeout,setInterval等

    • 其中的window属性会执行globalobject这个对象本身

  • 在解析过程中会把代码加入到 globalobject 这个对象里,并不会给属性赋值,只有当代码被执行时才会赋值,代码被运行前会从磁盘中取出再加入到内存,在内存中转为机器指令后又到cpu中执行

1
2
3
4
5
6
7
8
9
10
11
12
var GlobalObject = {
String: "类",
Date: "类",
Number: "类",
setTimeout: 函数,
setInterval: "函数",
window: this,
name: undefined,
num1: undefined,
num2: undefined,
result: undefined
}

8.3.2. 执行上下文栈

  • 内存会被划分为 栈结构堆结构
  • v8引擎内部会有一个执行上下文栈(execution contenxt stack,简称ECS),它是用于执行代码的调用栈,代码被执行前需要先放到这个栈结构中
  • 它执行的是全局的代码块
    • 全局的代码块为了执行会创建一个全局执行上下文栈(global execution context,GEC)
    • GEC会被放入到ECS中执行
  • GEC被放入到ECS中里面包含两部分内容:
    • 第一部分:在代码之前,在parser转成AST的过程中,会将全局定义的变量,函数等加入到GlobalObject中,但是并不会赋值
      • 这个过程也称为变量的作用域提升(hoisting)
    • 第二部分:在代码执行中,对变量赋值,或者执行其他的函数

8.3.3. 遇到函数如何执行

  • 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack

  • 函数执行上下文中包含三部分内容

    • 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)

      • AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
    • 第二部分:作用域链(scope chain)由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找

    • 第三部分:this绑定的值

8.4. 变量环境和记录

  • 上面的讲解都是基于早期ECMA的版本规范

    • Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
    • 每一个执行上下文会被关联到一个变量对象,在源代码中的便利和函数声明会被作为属性添加到VO中。对于函数来说,参数也会被添加到VO中
  • 在最新的ECMA的版本规范中,对于一些词汇进行了修改

    • Every execution context has an associated VariableEnvironment. Variables and functions declared in ECMAScript code evlauated in an execution context are added as bindings in that VariableEnvironment’s Environment Record. For function code, parameters are also added as bindings to that Environment Record.
    • 每一个执行上下文会关联到一个变量环境(VariableEnvironment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中
  • 通过上面的变化可以知道,在最新的ECMA标注中,前面的变量对象VO已经有另外一个称呼了变量环境VE

9. 作用域提升面试题

9.1. 1

1
2
3
4
5
6
7
var n = 100
function foo(){
n = 200
}
foo()
//200
console.log(n)

9.2. 2

1
2
3
4
5
6
7
8
9
10
function foo(){
//undefined
console.log(n)
var n = 200
//200
console.log(n)
}

var n = 100
foo()

在解析foo函数过程中,n会被加入到AO中,只是不会被赋值

9.3. 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var n = 100

function foo1(){
// 100
console.log(n)
}

function foo2(){
var n = 200
//200
console.log(n)
foo1()
}

foo2()

9.4. 4

1
2
3
4
5
6
7
8
var a = 100
function foo(){
// undefined
console.log(a)
return
var a = 100
}
foo()

在解析foo函数过程中,不论是否存在return,a都会被加入到AO中,只是不会被赋值

9.5. 5

1
2
3
4
5
6
7
function foo(){
var a = b = 100
}

foo()
console.log(a)
console.log(b)

本文结束  感谢您的阅读