浏览器之JS引擎工作机制
在解析 HTML 构建 DOM 树时, 渲染线程会解析到script标签, 则会将执行权交给 js 线程(引擎)来接管, 此时 js 引擎开始干活了,那么他到底是怎么个干法呢? 这里其实在作用域那一块已经提到了, 那里是以es3 的规范为基础说的, 这里我们按es6的新规范来理解...
js 引擎结构
内存堆heap
: 存储引用类型数据调用栈call stack
: 存储基础数据类型, 引用类型地址, 存放执行上下文(运行时)解释器
: 对源代码进行解释,编译,执行等
js引擎(V8)执行代码
- 1.源码转换为抽象语法树, 并生成执行上下文
# 步骤(不做细讲)
阶段一: 分词, 即词法分析
阶段二: 解析, 语法分析
阶段三: 生成 AST
- 2.生成字节码
解释器参与工作, 根据 AST 生成字节码
字节码: 介于字节码 和 机器码之间的代码
- 3.执行代码
解释器会逐行解释和执行字节码; 这里有个优化就是对于多次执行的代码(热点代码),
后台编译器(JIT)将会把这些字节码转换为机器码
执行上下文
当浏览器加载script的时候, js引擎开始工作;
此时会默认直接进入Global(全局上下文),将全局上下文push到引擎的调用栈call Stack
;
如果在代码中调用了函数,则会创建Function(函数上下文)并压入调用栈内,变成当前的执行环境上下文;
当执行完该函数,该函数的执行上下文便从调用栈弹出返回到上一个执行上下文.
- 执行上下文分类
全局: 当js文件加载进浏览器运行的时候,进入的就是全局执行上下文.
全局变量都是在这个执行上下文中,代码在任何位置都能访问.
函数: 定义在具体某个方法中的上下文,函数调用时就进入函数上下文.
局部变量就是在这个函数中访问
Eval: 定义在Eval方法中的上下文.
es3 的执行上下文
- 准备阶段(预解析)
executionContext = {
// 变量对象
'variableObject': {
'arguments声明': {
length: 0
},
'function声明': fn ,
'var声明': undefined
},
// 作用域链
'scopeChain': [
'自身的variableObject', ...'所有父级的executionContext'
],
// this 对象
'this': {}
}
// 初始化 arguments参数, 变量为 undefined, fn变量指向堆内存
- 执行阶段
为上述arguments参数, 变量等赋值, 运行代码
- 示例
var a = 2
function addAll(x,y) {
var b = 1;
console.log(add) // ƒ add(x,y) { return x + y }; 如果没有 add 函数声明,则是 undefined
function add(x,y) {
return x + y
}
var add = function(x,y) {
return x + y
}
console.log(add) // ƒ (x,y) { return x + y }
var result = add(x, y)
return a + b + result
}
addAll(100, 100)
es6 的执行上下文
可以看到es3中, js会存在变量提升以及隐式覆盖,for循环的i在全局等问题; 所以在 es6新规范 中引入了块级作用域和 let,const 关键字来规避之前的一些缺陷; 由于 js 需要向下兼容,所以还是会保留变量提升的特性
- 1,准备阶段(创建执行上下文)
executionContext = {
// 词法环境
blockContext: {
'let,const 声明': 'xxx'
},
// 变量环境
VO: {
/* var 等声明 */
},
// 外部引用(同作用域链)
outer: [
// ...
]
// this
this: {}
}
- 2.执行阶段
变量查找,赋值,执行
- 示例
function fn() {
var a = 1
// console.log(b) // Cannot access 'b' before initialization
let b
// 遇到let,const 声明, b会被提升到当前块级作用域顶部,
// 从顶部到声明处是暂时性死区, 无法操作b, 会报错
// 直接在解析词法环境下就报错了
{
let b = 3
var c = 4
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
}
fn() // a,b,c,d => 11, 3, undefined, 4
示例的上下文
变量环境 | 词法环境 |
---|---|
a = 1,c=4 | 块1: b = 3 |
块2: b = undefined |
异步
的JS
通过上面我们知道 JS 通过创建调用栈
和执行上下文环境
来执行一段 js 代码; 由于 js 设计的是单线程(因为JS主要目的用来实现很多交互相关的操作,如DOM相关操作,如果是多线程会造成数据的同步问题),只能从上往下执行单个任务,讲道理遇到耗时任务
就会阻塞了.那怎么呢?
js 通过回调函数
来处理这种耗时(异步)
任务;JS引擎其实并不提供异步的支持,异步支持主要依赖于运行环境(浏览器或Node.js)
浏览器下事件循环(node下的不一样)
当遇到耗时任务(Web APIs的调用
)时, 浏览器进程其实会维护一个任务(回调)队列
存放 Web APIs任务的回调函数;
另而JS引擎则会不断监听其主线程调用栈是否为空, 等到执行调用栈空了之后, 就会取出任务队列中的回调函数放到执行栈进行调用; 这种机制就是事件循环
...
任务(回调)队列
分类
上面任务(回调)队列里面其实存放的任务都成为宏任务
, 这类任务会通过回调函数的方式滞后执行;
浏览器中的宏任务
包括哪些呢:
渲染事件: DOM 解析, 布局, 绘制...(requestAnimationFrame)
用户交互事件: 点击,缩放,滚动等...click事件 等
script下JS执行事件: js执行
网络请求,文件读取(I/O): ajax请求
定时器: setTimeout
在宏任务中对时间的控制粒度都比较宽泛, 像页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间.
<!DOCTYPE html>
<html>
<body>
<div id='demo'>
<ol>
<li>test</li>
</ol>
</div>
</body>
<script type="text/javascript">
function timerCallback2(){
console.log(2)
}
function timerCallback(){
console.log(1)
setTimeout(timerCallback2,0)
}
setTimeout(timerCallback,0)
</script>
</html>
<!--
setTimeout 函数触发的回调函数都是宏任务; 在这期间,可能会插入其他系统任务
则会影响到第二个定时器的执行时间, 所以不够精确, 实时性太差
-->
为了解决这类问题, 于是引入了微任务
;
微任务就是一个需要异步执行的函数, 执行时机是在主函数执行结束之后、当前宏任务结束之前.
当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。顾名思义,这个微任务队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务,这时候就需要使用这个微任务队列来保存这些微任务了。不过这个微任务队列是给 V8 引擎内部使用的,所以你是无法通过 JavaScript 直接访问的。
- 在浏览器里面,产生微任务有两种方式
第一种方式是使用 MutationObserver 监控某个 DOM 节点,
然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,
当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject()
的时候,也会产生微任务
- 小结
1.微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列;
2.微任务的执行时长会影响到当前宏任务的时长(多个微任务时长累加);
3.在一个宏任务中,如果创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
任务执行顺序
举个例子, 看一下宏任务和微任务的执行顺序...
setTimeout(function() {
console.log('a')
});
new Promise(function(resolve) {
console.log('b')
for (var i = 0; i <1000; i++) {
i === 1000 && resolve()
}
}).then(function() {
console.log('c')
});
console.log('d');
// 1.首先执行script下的宏任务,遇到setTimeout,将其放入宏任务的队列里
// 2.遇到Promise,new Promise直接执行,打印b
// 3.遇到then方法,是微任务,将其放到微任务的队列里
// 4.遇到console.log('d'),直接打印
// 5.本轮宏任务执行完毕,查看微任务,发现then方法里的函数,打印c
// 6.本轮event loop全部完成。
// 7.下一轮循环,先执行宏任务,发现宏任务队列中有一个setTimeout,打印a
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。