前端基础 | js执行过程你了解多少?

说实话,之前真的不太了解这一块,我想也是大部分前端的问题吧,趁着刷题,巩固下基础知识。

js是单线程语言:

即:在浏览器中只有一个线程在执行js脚本代码。

这里所说的js是单线程,并不是说在js执行过程中只有一个线程。其实有四个线程,包括:

  1. JS引擎线程
  2. 事件触发线程
  3. 定时器触发线程
  4. HTTP异步请求线程

但永远只有 JS引擎线程 在执行js脚本,其他三个只是协助,不参与脚本解析和执行。

那么问题来了:为什么 JS 被设计成单线程?

js是单线程,但是代码解析不会发生阻塞

js是异步执行的,通过事件循环(Event Loop)的方式实现。

js引擎执行分三个阶段

注:浏览器首先按顺序加载由 <script> 标签分割的js代码块,加载js代码块完毕后,立刻进入以下三个阶段,然后再按顺序查找下一个代码块,再继续执行以下三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。

  1. 语法分析
  2. 预编译阶段
  3. 执行阶段

语法分析

分析脚本代码块语法是否正确,正确则进入「预编译阶段」,否则抛出 SyntaxError, 停止该代码块代码继续执行,然后继续查找下一个代码块。

预编译阶段

js的运行环境(执行上下文)主要有三种:

  • 全局环境(js代码加载完毕后,进入预编译阶段即进入了全局环境)
  • 函数环境(函数执行时进入该函数环境,不同函数的函数环境不同)
  • eval环境(不建议使用,会有安全,性能等问题)

执行栈,又称调用栈,用来存贮代码执行期间创建的所有执行上下文。是一个遵循后进先出(LIFO)的结构。栈底永远是全局执行上下文,栈顶是当前执行上下文。

执行上下文分两个阶段创建:1)创建执行上下文; 2)执行阶段

创建执行上下文

  1. 创建变量对象
  2. 创建作用域链
  3. 确定 this 指向

创建变量对象

主要过程如下:

图片描述

  1. 创建 Augments 对象:检查当前上下文中的参数,建立对象的属性与值,仅在函数环境(非箭头函数)执行,全局环境没有这个过程。
  2. 检查 Funchtion 函数声明,创建属性:按代码顺序查找,将找到的函数提前声明,若函数不存在则新建立属性和属性值(指向该函数内存地址的引用);若存在,则直接覆盖原来的。
  3. 检查 var 变量,声明创建属性:按代码顺序查找,将找到的变量提前声明,如果变量不存在,则赋值为:undefined。若存在,则忽略该声明。

注:在全局环境中,window 对象就是全局执行上下文的变量对象,所有的变量和函数都是 window 对象的属性方法。

所以函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明

变量提升:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 letconst 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因。

「变量对象」转化为「活动对象」后才能进行访问

创建作用域链

作用域链由当前执行环境的变量对象和上层的一系列活动对象组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var num = 30;

function test() {
var a = 10;

function innerTest() {
var b = 20;

return a + b
}

innerTest()
}

test()

在上面的例子中,当执行到调用 innerTest 函数,进入 innerTest 函数环境。全局执行上下文和 test 函数执行上下文已进入执行阶段,innerTest 函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是 AO(global)AO(test)VO(innerTest) ,而 innerTest 的作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,如下:

1
2
3
4
5
6
7
8
9
10
11
innerTestEC = {

//变量对象
VO: {b: undefined},

//作用域链
scopeChain: [VO(innerTest), AO(test), AO(global)],

//this指向
this: window
}
  • 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象);
  • 最后一项永远是全局作用域(全局执行上下文的活动对象);
  • 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。

确定 this 指向

全局环境下指向window

函数环境下需根据执行环境和执行方法确定

执行阶段

js进入执行阶段后,代码执行顺序如下:

宏任务(同步任务) –> 微任务 –> 宏任务(异步任务)

图片描述

1.宏任务:宏任务又按执行顺序分为同步任务和异步任务

  • 同步任务: 在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)
  • 异步任务: 不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列( task queue ),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步 AjaxDOM 事件,setTimeout 等。

理解宏任务中同步任务和异步任务的执行顺序,那么就相当于理解了JS异步执行机制–事件循环( Event Loop

事件循环可以理解为由三部分组成:主线程执行栈异步任务等待触发任务队列

在JS引擎主线程执行过程中:

  1. 首先执行宏任务的同步任务,在主线程上形成一个执行栈,可理解为函数调用栈;
  2. 当执行栈中的函数调用到一些异步执行的 API (例如异步 AjaxDOM 事件,setTimeoutAPI),则会开启对应的线程( Http 异步请求线程,事件触发线程和定时器触发线程)进行监控和控制
  3. 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列( task queue )中,等待主线程读取执行
  4. 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
  5. 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环( Event Loop )的过程

2.微任务:是在 es6node 环境中出现的一个任务类型.

微任务的 API 主要有: Promiseprocess.nextTick

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('script start');

setTimeout(function() {
console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

console.log('script end');

输出结果:

1
2
3
4
5
6
script start
script end
promise1
promise2
setTimeout

以上就是js执行的过程

参考文章:

js引擎的执行过程(一)

js引擎的执行过程(二)