1. 从输入 URL 到页面展示 发生了什么?

​ 总体分为以下过程:

  • DNS 域名解析:将域名解析成 IP 地址
  • TCP 连接:TCP 三次握手
  • 发送 HTTP 请求
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析渲染页面
  • 断开连接:TCP 四次挥手

2. 浏览器工作原理

在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?

大致流程如下:

  • 首先,用户输入服务器地址,与服务器建立连接
  • 服务器返回对应的静态资源(index.html)
  • 然后浏览器拿到 index.html 后进行解析
  • 当解析时遇到 css 或 js 文件,就向服务器请求并下载对应的 css 和 js 文件
  • 最后浏览器对页面进行渲染,执行 js 代码

3. 浏览器渲染过程

image-20220325160129577.png

  1. HTML Parser 将 HTML解析转换成 DOM 树

  2. CSS Parser 将 样式表转换成 CSS 规则树

  3. 合并 DOM 树和 CSS 规则树,生成 render(渲染) 树

  4. 布局 render 树(Layout)

    通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸

  5. 绘制 render 树(painting),进行 Display 展示

注意图中顶部的紫色 DOM 三角形,实际上是 js 对 DOM 的相关操作。

4. 一个强大的 JavaScript 引擎 — V8 引擎

在解析 HTML 的过程中,遇到了 JavaScript 标签,该怎么办呢?

  • 会停止解析 HTML ,而去加载和执行 JavaScript 代码

那么,JavaScript 代码由谁来执行呢?

  • JavaScript 引擎

    高级的编程语言最终都要转成机器指令来执行的,

    所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行

(1)V8 引擎的架构

image-20220325165859884.png

V8 的底层架构主要有三个核心模块(Parse、Ignition、TurboFan)

1. Parse

该过程主要是对 JavaScript 源代码进行词法分析语法分析

词法分析:对代码中的每一个词每一个符号进行解析,最终生成很多 tokens

例如:对 const name = “curry”

// 首先对const进行解析,因为const为一个关键字,所以类型会被记为一个关键词,值为const
tokens: [
{ type: 'keyword', value: 'const' }
]

// 接着对name进行解析,因为name为一个标识符,所以类型会被记为一个标识符,值为name
tokens: [
{ type: 'keyword', value: 'const' },
{ type: 'identifier', value: 'name' }
]

// 以此类推...

语法分析:在词法分析的基础上,拿到 tokens 中的一个个对象,根据不同类型,再进一步分析具体语法,最终生成 AST 抽象语法树

可以详细查看通过 Parse 转换后的 AST 的工具:AST Explorer

2. Ignition

一个解析器,可以将 AST 转换成 ByteCode(字节码)

3. TurboFan

一个编译器,可以将字节码编译为 CPU 认识的机器码

(2)V8 引擎的执行过程

image-20220325171051967.png

  • Blink 内核将 JS 源码交给 V8 引擎
  • Stream 获取到 JS 源码进行编码转换
  • Scanner 进行词法分析,将代码转换成 tokens
  • Parser 和 PreParser
    • Parser :直接解析,将 tokens 转成 AST 树
    • PreParser:预解析,对不必要的函数进行预解析,也就是只解析暂时需要的内容,而在函数被调用时才进行函数的全量解析
  • 生成 AST 树后,会被 Ignition 转成字节码,之后就是代码的执行过程

5. JavaScript 的执行过程

假如要执行如下代码:

var title = "hello"
console.log(num1)
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(result)

(1)首先,代码被解析,V8 引擎内部会帮助我们创建一个全局对象:Global Object(GO)

  • GO 可以访问所有的作用域

  • 里面会包含 Date、Array、String、setTimeout等等(所以我们可以直接 new Date() )

  • GO 还有一个window 属性指向自己(所以window.window.window还是指向 GO自己)

用伪代码表示为:

var globalObject = {
String: 类,
setTimeout: 函数,
...
window: globalObject
}

(2)然后运行代码

  1. 首先我们要知道 js 引擎内部有一个执行上下文栈(Execution Context Stack,简称 ESC),它是用于执行代码的调用栈。

  2. 为了全局代码能够正常执行,首先需要创建一个**全局执行上下文 **(Global Execution Context,简称GEC),全局代码需要被执行时才会创建

  3. 然后全局执行上下文会被放入执行上下文栈中执行,包含两个部分:

    • 在代码执行前,会将全局定义的变量,函数等加入到 GlobalOject 中,但是并不会赋值(也称为变量的作用域提升

image-20220325191605422.png

  • 开始依次执行代码:

    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"

function foo() {
console.log(message) // Hello Global
}

function bar() {
var message = "Hello Bar"
foo()
}

bar()

比如说这里,当执行 foo 函数的时候, foo 的 AO 中没有message,就会去它的父级 VO 中查找。一定要注意!当第一次代码解析的时候, foo 的函数执行上下文就已经确定了,其中包括三部分:

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

    其中包含形参、arguments、函数定义、指向函数对象或定义的变量

  • 第二部分:就是作用域链

  • 第三部分:this 绑定的值

因此!当foo在自己的AO找不到message的时候,去父级 VO 找,这个父级 VO 就是一开始解析时候保存的 GO,所以message为 “Hello Global”

简单描述一下这个过程吧:(我不专业的表达)

// 初始化 GO
GO: {window; message:undefined; foo: 地址1; bar: 地址2;}
// 执行代码
GO: {window; message:"Hello Global"; foo: 地址1; bar: 地址2;}
bar函数执行,创建一个函数执行上下文,
其中包括 VO对象: AO:{message: undefined}
然后开始执行 bar函数
message:"Hello Bar" (赋值)
foo()
foo函数执行,创建一个foo的函数执行上下文
其中包括:VO: AO: {}
然后开始执行代码
console.log(message)
发现自己的AO没有message,会向上找,即从自己保存的父级VO中查找,找到GO中的message为 "Hello Global"

image-20220325231532928.png
(图来源于coderwhy)

JS执行上下文:https://github.com/mqyqingfeng/Blog/issues/8

写的挺清晰详细的

几道常见的作用域提升面试题:

var n = 100
function foo() {
n = 200
}
foo()
console.log(n) // 200
function foo() {
console.log(n) // undefined
var n = 200
console.log(n) // 200
}

var n = 100
foo()
var a = 100

function foo() {
console.log(a) // undefined
return
var a = 200
}

foo()
function foo() {
m = 100
}

foo()
console.log(m) // 100
function foo() {
var a = b = 10
// => 转成下面的两行代码
// var a = 10
// b = 10
}

foo()

//console.log(a) // 报错 a is not defined(因为当 foo函数执行完之后,foo的函数执行上下文就会弹出栈(没啦!哪里还会有a呢))
console.log(b) // 10