JavaScript的运行机制

目录
  1. 1. 执行环境(Execution Context)
  2. 2. 执行环境栈(Execution Context Stack)
  3. 3. 执行环境内部详情
  4. 4. 事件循环(Event Loop)
  5. 5. timer中的0ms和1ms
  6. 6. Node中的Event Loop
  7. 7. 参考

执行环境(Execution Context)

JS代码可以归为下面三种情况之一:

  • 全局代码:首先执行这里的代码
  • 函数代码
  • Eval代码:eval()函数中的文本

默认有一个全局执行环境,只能有一个全局执行环境

执行函数会创建一个新的执行环境,可以有多个函数执行环境

执行环境栈(Execution Context Stack)

JS引擎默认进入全局执行环境执行全局代码,如果在全局代码中调用了一个函数,就会创建一个新的执行环境,并添加到栈的顶部,JS引擎始终执行栈顶部的执行环境,一旦函数执行完成,当前的执行环境就会被弹出, JS引擎继续执行下一个执行环境,直到再次到达全局执行环境

执行环境内部详情

调用执行环境分两个阶段

1.创建阶段

  • 1.创建作用域链
  • 2.创建变量、函数和参数
  • 3.确定this的值

2.执行阶段

从上到下逐行执行代码。

从概念上来看,执行环境就像一个对象,包含三个属性

1
2
3
4
5
6
7
8
9
executionContextObj = {
// 作用域链
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },

// 变量对象
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },

'this': {}
}

执行环境的执行流程如下:

  • 1.创建执行环境

  • 2.进入创建阶段

  • 3.初始化作用域链

  • 4.创建变量对象

  • 5.创建arguments对象,arguments对象创建与参数名相同的属性,以引用的方式指向参数

1
2
3
4
5
6
7
8
function a(x, y) {
console.log(arguments); // { '0': 1, '1': 2 }
x = 3;
y = 4;
console.log(arguments); // { '0': 3, '1': 4 }
}

a(1, 2);
  • 6.扫描环境内的函数声明

  • 7.每发现一个函数声明,在变量对象中创建与函数名相同的属性,值是指向这个函数的指针

  • 8.如果这个函数名已存在,则重写这个指针的值

  • 9.扫描环境内的变量声明

  • 10.每发现一个变量声明,在变量对象中创建与变量名相同的属性,值初始化为undefined

  • 11.如果变量名已存在,则什么也不做

  • 12.确定this的值

  • 13.进入执行阶段

  • 14.逐行执行代码

事件循环(Event Loop)

JS是单线程,为了不阻塞线程,JS通过事件循环的方案解决耗时任务。

任务分两种:

  • 1.宏任务(Macrotask):script、setTimeout、setInterval、setImmediate、I/O、UI交互事件
  • 2.微任务(Microtask):Promise、process.nextTick、MutaionObserver

对应的任务队列分别是宏任务队列(Macrotask Queue)微任务队列(Microtask Queue)

一开始执行的<script>就属于宏任务

Event Loop将一个宏任务压入执行环境栈,在执行环境栈执行的过程中,如果遇到任务,则放到相应的任务队列中,全部出栈后,清空所有的微任务,依此循环往复。

注意,微任务中会优先清空next tick queue,即通过 process.nextTick 注册的函数。

timer中的0ms和1ms

不管是浏览器还是Node,最低延时是1ms

1
2
3
4
5
6
7
setTimeout(function () {
console.log(0);
}, 1);

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

打印:0 1

原因如下(Blink源码和Node源码):

1
2
3
// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93

double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
1
2
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior

Node中的Event Loop

在 Node 中,如果延时相同,则会被合并成一个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(function () {
new Promise(function (resolve) {
console.log(1);
resolve();
}).then(function () {
console.log(2);
});
});

setTimeout(function () {
new Promise(function (resolve) {
console.log(3);
resolve();
}).then(function () {
console.log(4);
});
});

在 Chrome 打印:1 2 3 4

但在 Node 中打印:1 3 2 4

参考

  1. What is the Execution Context & Stack in JavaScript?
  2. Event Loop的规范和实现