JS引擎(V8为例)
`JavaScirpt 引擎主要用来将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)能识别的对应的汇编代码。同时,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。
最出名的JS引擎当属 Google V8。
V8 引擎是用 C ++ 编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中,比如 Node。
V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中最重要的 4 个模块是:
- Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
- Ignition:interpreter,解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型。
- TurboFan:compiler,即编译器,利用 Ignitio 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
- Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。
总结下来就是:Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)。
在 V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了即时编译(JIT)的双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。
即时编译(Just-in-time compilation),简称为 JIT。指可以直接执行源码(比如:node test.js),但是在运行的时候先编译再执行,这种方式被称为JIT。V8 也属于 JIT 编译器。
解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
V8 执行一段 JavaScript 代码所经历的主要流程可总结为:
- 初始化基础环境;
- 解析源码生成 AST 和作用域;
- 依据 AST 和作用域生成字节码;
- 解释执行字节码;
- 监听热点代码;
- 优化热点代码为二进制的机器代码;
- 反优化生成的二进制机器代码。
V8 有对应的 D8工具。它是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。
此外,V8引擎内部还做了一系列优化措施:
- 惰性解析基础上,增加预解析器来解决了闭包所带来的外部变量无法释放的问题。
- 引入快属性,慢属性机制,提升对象属性的访问速度。
- 通过内联缓存来提升函数执行效率。
- 引入字节码,相对二进制码,降低了时间和空间成本。
具体优化细节可参考下面文献:浏览器是如何工作的:Chrome V8让你更懂JavaScript
下面,我们主要探究一下JS的异步代码处理机制。
JS异步
- 我们都知道,
JS
引擎 是单线程设计。它的创造者就是单纯为了keep it simple
。 - 我们也知道,
JS
代码可以分为同步代码
和异步代码
。
常见的 异步代码
生产者有:
seTimeout
setInterval
Dom
事件ajax
/fetch
请求process.nextTick
(Nodejs
特有)等
处理 异步代码
的方式有:
callBack
promise
async/wait
- 发布/订阅 (观察者模式)
为了异步处理一些耗时的操作,JS引擎
又是基于 事件循环(Event Loop)
机制(单独的事件触发线程
处理),实现 非阻塞I/O
的。那么,Event Loop
机制如何工作呢:
JS
将执行环境分为执行栈
和任务队列
。首先,当前代码块所有代码被放到
执行栈
中 自上而下 执行;当遇到异步操作,将异步
API
中定义的 回调函数 作为任务
,添加到任务队列
中;当
执行栈
中的同步代码
全部执行完,处于空闲态后,会去循环处理任务队列
里的任务;将
任务队列
里的任务
按先来后到的顺序依次放到执行栈
中执行。当
任务
代码中遇到异步代码,再次放入任务队列
;如此往复,称为
事件循环(Event Loop)
;
注意:
熟悉
Promise
原理就会很清楚:Promise
构造函数代码是同步代码
。异步是体现在then
和catch
块中。在
async/await
中,await
出现之前的代码也是立即执行的同步代码
。之后的代码是放入任务队列
的异步代码
。
// async/await 本身就是promise+generator的语法糖。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
//等价于
function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
另外,任务队列
中的 异步任务
又分为 宏任务
和微任务
宏任务
宏任务(macrotask
),也叫tasks
。以下 异步任务产生的回调会被放入 宏任务
队列:
setTimeout
setInterval
I/O
操作UI rendering
(浏览器独有)requestAnimationFrame
(浏览器独有)setImmediate
(Node
独有)
微任务
微任务(microtask
),也叫jobs
。以下 异步任务
会被放入 微任务
队列:
Promise
async/await
MutationObserver
window.queueMicrotask()
process.nextTick
(Node
独有)
MutationObserver
: 一个用来监视DOM
变动的API
。DOM
的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个API
都可以得到通知。了解更多)
queueMicrotask()
: 为了允许第三方库、框架、polyfills 能使用微任务,Window 暴露了 queueMicrotask() 方法。
下面,我们来看看 分 宏任务队列
和 微任务队列
的Event Loop
执行顺序:
所有代码被放到
执行栈
中 自上而下 执行;遇到 异步操作
API
,根据API
的类型, 将 回调函数 添加到任务队列
中宏任务队列
或者微任务队列
;当
执行栈
中的 同步代码 全部执行完,处于空闲态后,先去循环微任务队列
里的函数;依次将
微任务队列
里的函数 放到执行栈
中执行,如果过程中产生新的微任务,也会放入微任务队列的末尾,并且在此次循环中执行完成。当
微任务队列
里的函数全部执行完成,才会将宏任务队列
里的函数按顺序放到执行栈
中执行。当执行完当前的
宏任务
时,只有当微任务队列
为空的时候,才会继续执行下一个宏任务
。也就是说,在执行宏任务
的时候产生了新的微任务
,那么在这个宏任务
执行完成以后,依然优先处理微任务
。如此往复。直到所有
任务队列
为空。
重要:每一个宏任务执行完毕,会检查渲染任务列表,如果有渲染任务,
GUI
线程会接管渲染,渲染完成后,JS
线程继续接管。
Nodejs 有所不同
NodeJS
的 异步操作 也分宏任务
和微任务
。宏任务
分为 6 个阶段,4个队列。微任务
分为 2 个队列。
宏任务
6个阶段:
timers
阶段:这个阶段执行setTimeout
和setInterval
设置的callback
。I/O callback
阶段:执行[close事件、timers、setImmediate()] 设定的callbacks之外的其他callbacks
。idle, prepare
阶段:仅node
内部使用。poll
阶段:获取新的I/O
事件,适当的条件下node
将阻塞在这里。check
阶段:执行setImmediate()
设定的callbacks
。close
阶段:执行socket.on('close', ....)
这些callbacks
。
4个队列:
iTimers
:setTimeout
,setInterval
iIO Callbacks
:other……
iCheck
:setImmediate()
iClose
:socket.on('close', ....)
微任务
2个队列
Next Tick
:是放置process.nextTick(callback)
的回调任务.Other Micro
:放置其他微任务
,比如Promise
等。
Node.js 中的 EventLoop 过程
NodeJS 11
之前:
- 执行全局
Script
的同步代码。 - 执行
微任务
,先执行所有Next Tick Queue
中的所有任务,再执行Other Microtask Queue
中的所有任务。 - 开始执行
宏任务
,共6
个阶段,从第1
个阶段开始执行。每一个阶段的宏任务
全部 执行完成后,回去执行所有微任务
(同上),再执行 下个阶段 的全部宏任务
。 - 这就是
NodeJs
的Event Loop
。
Node 11 +
的变化:
宏任务还是分阶段依次执行,但是每一个阶段的每一个
宏任务
执行完,都回去执行所有微任务
,再继续执行下一个宏任务
。而不是等每个阶段宏任务
全部执行完才回去执行微任务。和浏览器更加趋同.
执行顺序自测
浏览器端输出顺序:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
NodeJS中输出顺序:
console.log(1);
setTimeout(() => {
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise((resolve) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
});
});
new Promise((resolve) => {
console.log(7);
resolve();
}).then(() => {
console.log(8);
});
process.nextTick(() => {
console.log(6);
});
setTimeout(() => {
console.log(9);
process.nextTick(() => {
console.log(10);
});
new Promise((resolve) => {
console.log(11);
resolve();
}).then(() => {
console.log(12);
});
});
//node <11: 1 7 6 8 2 4 9 11 3 10 5 12
// node>=11: 1 7 6 8 2 4 3 5 9 11 10 12