3

浏览器之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

a_dodo
2.4k 声望1k 粉丝

天下事有难易乎?