adodo0829 / blog

搭建知识体系
29 stars 4 forks source link

浏览器之JS引擎工作机制&事件循环 #40

Open adodo0829 opened 4 years ago

adodo0829 commented 4 years ago

浏览器之JS引擎工作机制,事件循环

在解析 HTML 构建 DOM 树时, 渲染线程会解析到script标签, 则会将执行权交给 js 线程(引擎)来接管, 此时 js 引擎开始干活了,那么他到底是怎么个干法呢? 这里其实在作用域那一块已经提到了, 那里是以es3 的规范为基础说的, 这里我们按es6的新规范来理解...

js 引擎结构

内存堆heap: 存储引用类型数据 调用栈call stack: 存储基础数据类型, 引用类型地址, 存放执行上下文(运行时) 解释器: 对源代码进行解释,编译,执行等

js引擎(V8)执行代码

函数: 定义在具体某个方法中的上下文,函数调用时就进入函数上下文. 局部变量就是在这个函数中访问

Eval: 定义在Eval方法中的上下文.

#### es3 的执行上下文
- 准备阶段(预解析)   
```js
executionContext = {
  // 变量对象
  'variableObject': {
    'arguments声明': {
      length: 0
    },
    'function声明': fn ,
    'var声明': undefined
  },
  // 作用域链
  'scopeChain': [
    '自身的variableObject', ...'所有父级的executionContext' 
  ],
  // this 对象
  'this': {}
}
// 初始化 arguments参数, 变量为 undefined, fn变量指向堆内存

浏览器下事件循环(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 直接访问的。

第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务

- 小结

1.微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列;

2.微任务的执行时长会影响到当前宏任务的时长(多个微任务时长累加);

3.在一个宏任务中,如果创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

#### 任务执行顺序
举个例子, 看一下宏任务和微任务的执行顺序...
```js
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