01_深入JS运行原理
1. 从输入 URL 到页面展示 发生了什么?
总体分为以下过程:
- DNS 域名解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
2. 浏览器工作原理
在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?
大致流程如下:
- 首先,用户输入服务器地址,与服务器建立连接
- 服务器返回对应的静态资源(index.html)
- 然后浏览器拿到 index.html 后进行解析
- 当解析时遇到 css 或 js 文件,就向服务器请求并下载对应的 css 和 js 文件
- 最后浏览器对页面进行渲染,执行 js 代码
3. 浏览器渲染过程
HTML Parser 将 HTML解析转换成 DOM 树
CSS Parser 将 样式表转换成 CSS 规则树
合并 DOM 树和 CSS 规则树,生成 render(渲染) 树
布局 render 树(Layout)
通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸
绘制 render 树(painting),进行 Display 展示
注意图中顶部的紫色 DOM 三角形,实际上是 js 对 DOM 的相关操作。
4. 一个强大的 JavaScript 引擎 — V8 引擎
在解析 HTML 的过程中,遇到了 JavaScript 标签,该怎么办呢?
- 会停止解析 HTML ,而去加载和执行 JavaScript 代码
那么,JavaScript 代码由谁来执行呢?
JavaScript 引擎
高级的编程语言最终都要转成机器指令来执行的,
所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行
(1)V8 引擎的架构
V8 的底层架构主要有三个核心模块(Parse、Ignition、TurboFan)
1. Parse:
该过程主要是对 JavaScript 源代码进行词法分析和语法分析。
词法分析:对代码中的每一个词每一个符号进行解析,最终生成很多 tokens
例如:对 const name = “curry”
// 首先对const进行解析,因为const为一个关键字,所以类型会被记为一个关键词,值为const |
语法分析:在词法分析的基础上,拿到 tokens 中的一个个对象,根据不同类型,再进一步分析具体语法,最终生成 AST 抽象语法树
可以详细查看通过 Parse 转换后的 AST 的工具:AST Explorer
2. Ignition
一个解析器,可以将 AST 转换成 ByteCode(字节码)
3. TurboFan
一个编译器,可以将字节码编译为 CPU 认识的机器码
(2)V8 引擎的执行过程
- Blink 内核将 JS 源码交给 V8 引擎
- Stream 获取到 JS 源码进行编码转换
- Scanner 进行词法分析,将代码转换成 tokens
- Parser 和 PreParser
- Parser :直接解析,将 tokens 转成 AST 树
- PreParser:预解析,对不必要的函数进行预解析,也就是只解析暂时需要的内容,而在函数被调用时才进行函数的全量解析
- 生成 AST 树后,会被 Ignition 转成字节码,之后就是代码的执行过程
5. JavaScript 的执行过程
假如要执行如下代码:
var title = "hello" |
(1)首先,代码被解析,V8 引擎内部会帮助我们创建一个全局对象:Global Object(GO)
GO 可以访问所有的作用域
里面会包含 Date、Array、String、setTimeout等等(所以我们可以直接 new Date() )
GO 还有一个window 属性指向自己(所以window.window.window还是指向 GO自己)
用伪代码表示为:
var globalObject = { |
(2)然后运行代码
首先我们要知道 js 引擎内部有一个执行上下文栈(Execution Context Stack,简称 ESC),它是用于执行代码的调用栈。
为了全局代码能够正常执行,首先需要创建一个**全局执行上下文 **(Global Execution Context,简称GEC),全局代码需要被执行时才会创建
然后全局执行上下文会被放入执行上下文栈中执行,包含两个部分:
- 在代码执行前,会将全局定义的变量,函数等加入到 GlobalOject 中,但是并不会赋值(也称为变量的作用域提升)
开始依次执行代码:
title = “hello” // 赋值
console.log(num1) // undefined, 不会报错
num1= 20 …
遇到函数如何执行?
先根据函数体创建一个函数执行上下文,并且压入到执行上下文栈中(EC Stack)
在初始化 GO 的时候,函数的 AO 也是会被初始化的
比如说 ,全局中,有function foo() {},一开始初始化GO的时候从上到下执行到 foo
会在内存中创建foo,并且保存作用域链到foo的内部属性[[scope]],即
foo.[[scope]] = [
globalContext.VO
]
作用域链?
由 VO(变量对象,在函数中就是 AO 对象)和 父级 VO组成,查找时会一层层查找
看一个例子:
var message = "Hello Global" |
比如说这里,当执行 foo 函数的时候, foo 的 AO 中没有message,就会去它的父级 VO 中查找。一定要注意!当第一次代码解析的时候, foo 的函数执行上下文就已经确定了,其中包括三部分:
第一部分:在解析函数成为 AST 树结构的时候,会创建一个 AO(Activation)
其中包含形参、arguments、函数定义、指向函数对象或定义的变量
第二部分:就是作用域链
第三部分:this 绑定的值
因此!当foo在自己的AO找不到message的时候,去父级 VO 找,这个父级 VO 就是一开始解析时候保存的 GO,所以message为 “Hello Global”
简单描述一下这个过程吧:(我不专业的表达)
// 初始化 GO |
(图来源于coderwhy)
JS执行上下文:https://github.com/mqyqingfeng/Blog/issues/8
写的挺清晰详细的
几道常见的作用域提升面试题:
var n = 100 |
function foo() { |
var a = 100 |
function foo() { |
function foo() { |