JavaScript宏任务,微任务与Event-loop

浏览器进程(chrome)

chrome中有5个主要进程:

  1. 浏览器进程(Browser Process):顶层进程,负责浏览器各进程工作,Tab外的工作由它负责。
    1. UI Thread:负责浏览器按钮、地址栏。
    2. storage Thread:负责文件访问。
  2. 渲染器进程(Renderer Process):浏览器内核,负责Tab内的所有工作。
    1. Main Thread :构建dom树 -> 加载资源 -> js下载与执行 -> 样式计算 -> 构建布局树 -> 绘制 -> 创建层树。(注:Main不是一个线程,而是多个线程的集合,为了方便介绍先聚合一下,后面展开讲)。
    2. Worker Thread: Web Worker 运行在这个线程,可能存在多个。
    3. Compositor Thread: 合成器,将层合成帧,分成多个磁贴。
    4. Raster Thread: 栅格化磁贴后交给GPU。
  3. 网络进程(Network Process):负责真正的发送http请求,接收和发送网络请求。
  4. 插件控制进程(Plugin Process):控制所有的插件。
  5. GPU进程(GPU Process):其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3DCSS的效果,只是随后网页、Chrome的UI界面都选择采取GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome也引入了GPU进程。

Main线程

Main线程是一些线程的集合,主要用于整个网页的工作。

其包括:

  1. GUI渲染线 程:

    负责渲染工作,包括解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

    注意:GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存等到JS引擎空闲时立即被执行。

  2. JS引擎线程:

    JS引擎线程负责解析Javascript脚本,运行代码(比如Chrome的V8)。

    一个Tab页内中无论什么时候都只有一个JS线程在运行JS。

    因为GUI渲染线程与JS引擎线程是互斥的,所以当JS执行的时间过长,页面的渲染也会阻塞。

  3. 事件触发线程:

    主要用来控制事件循环,添加回调事件到队列中。

    当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会被添加到事件线程中。 当对应的事件符合触发条件并被触发时,该线程会把事件添加到队列的队尾,等待JS引擎的处理

    注:由于JS单线程的关系,所以这些队列中的事件都得等JS引擎空闲了才会被执行

  4. 定时触发器线程:

    setIntervalsetTimeout执行的线程。

    由于js引擎是单线程的,如果由js来计时会影响计时准确性,因此额外使用一个线程来计时并触发定时。

    但是需要注意的是:

    setIntervalsetTimeout的计时并不是很准确的,其误差在(10~20ms):

    1. 再chrome底层中规定setInterval的最低时间为4ms。
    2. windows等系统底层的时间并不是完全准确的,普通的时间API误差在10~15ms(部分情况)。
    3. 由于JavaScript引擎是单线程,即使回调完全准确的将任务加入执行队列,但是前面的任务的执行时间仍然会增加回调任务的误差。
  5. 异步http请求线程:

    XMLHttpRequest连接后会新开一个线程。 将检测到状态变更时,如果设置有回调函数,该线程就产生状态变更事件。

    当然,实际做请求工作的还是 Network Process

JavaScript单线程

JavaScript的一大特点就是单线程设计,这一特点也造成了JavaScript的众多特性。这样设计可以使程序的开发更加简单,因为其不会涉及线程的通信,管理,比如在操作DOM的时候,如果是多线程,就需要组织各个线程的先后关系,但是单线程就不会涉及这些问题。这也使得JavaScript成为了一门入门比较简单的语言。

同步与异步

而JavaScript的任务又分为同步任务和异步任务。

同步任务是指直接按照代码顺序将其加入到执行栈中任务。

而异步任务是指需要一定的时间才能完成,并且这段时间的操作不是JavaScript线程能够控制的,这个时候则需要其他线程予以辅助。比如上面的定时触发器线程,异步http请求线程等。都可以帮助JavaScript完成定时器和http请求的异步任务。而上面的事件触发线程则用来在异步任务完成时,将回调函数添加到执行栈中。其基本过程如图:

同步-异步

执行栈(Execution Stack)

当我们调用一个方法的时候,js会生成一个与这个方法相对应的执行环境,也叫执行上下文,这个执行环境存在着这个方法的私有作用域、参数、this对象等等。因为js是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,js会先解析这些方法,把其中的任务按照执行顺序排队到一个地方,这个地方叫做执行栈。

事件表格(Event Table)

JavaScript保有的一中数据结构,它会存储所有的延迟事件(回调函数)。在对应异步操作完成过后,会由事件触发线程将对应的回调函数添加到事件队列中等待执行。

事件队列(Event queue)

当我们发出一个ajax请求或其他异步操作的时候,他并不会立刻返回结果,为了防止浏览器出现假死或者空白,主线程会把这个异步任务挂起(pending),继续执行执行栈中的其他任务,等异步任务返回结果后,js会将这个异步任务按照执行顺序,加入到与执行栈不同的另一个队列,也就是事件队列。

Event-loop

Event-loop Definitions:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop, which is unique to that agent.

翻译:为了协调事件、用户交互、脚本、渲染、网络等等,用户代理必须使用本节中描述的事件循环。每个代理都有一个相关的事件循环,该事件循环对该代理来说是唯一的。

每个Event-loop包含:

  • 正在运行任务,它可以是一个任务或者null。并且在最初的时候,其是一个null。其实为了处理可重入性。
  • 一个微任务队列,这是一个微任务队列,起初是空的。
  • 有一个标志微任务检查点的布尔值。

HTML Living Standard

Agent Definitions:

An agent comprises a set of ECMAScript execution contexts, an execution context stack, a running execution context, an Agent Record, and an executing thread. Except for the executing thread, the constituents of an agent belong exclusively to that agent.

翻译:代理包括一组ECMAScript执行上下文、一个执行上下文堆栈、一个正在运行的执行上下文、一个代理记录和一个正在执行的线程。除执行线程外,代理的组成部分专属于该代理。

HTML Living Standard

所谓事件循环,就是浏览器中(此处只谈浏览器,不涉及Node)各项任务(同步任务,异步任务)的执行次序之间的协调。其基本过程如下图:

event-loop

其基本过程:

  1. 主线程运行的时候会生成堆(heap)和栈(stack);

  2. js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;

  3. 当程序调用外部的API时,比如ajax、setTimeout等,会将此类异步任务挂起,继续执行执行栈中的任务,等异步任务返回结果后,再按照执行顺序排列到事件队列中;

  4. 主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。

  5. 主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的… …。

    这个过程被称为“Event Loop 事件循环”。

宏任务、微任务

首先需要明确的是:宏任务和微任务都是异步任务,其不同在于回调执行的时机。

在标准中,任务是分为taskmicrotask,任务和微任务。全文中提到宏任务(Macro task)的只有两处:

Unlike other algorithms in this and other specifications, which behave similar to programming-language function calls, spin the event loop is more like a macro, which saves typing and indentation at the usage site by expanding into a series of steps and operations.

翻译:与本规范和其他规范中的其他算法(其行为类似于编程语言函数调用)不同,spin事件循环更像是一个,它通过展开成一系列步骤和操作来节省使用站点上的输入和缩进。

可能是由于这一处,我们多用macro task来描述task

而在HTML Standard中,对于task的解释为:

形式上,一个task是一个包含如下内容的结构体:

  • steps:完成该任务需要的一系列步骤。
  • A source:任务源之一,用于对相关任务进行分组和序列化。
  • A document:与任务相关联的文档,对于不在窗口事件循环中的任务,则为空。
  • A script evaluation environment settings object set:一组环境设置对象,用于在任务期间跟踪脚本评估。

对于microtask定义有:

A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.

翻译:微任务是指通过微任务算法队列创建的任务。

从执行顺序上来看两者的不同:

任务执行

可以明确的是微任务是在一个宏任务结束后进行,此时即为上文提到的checkpoint,查看是否存在可执行的微任务。

具体宏任务和微任务为:

  1. 宏任务
    • 整体script
    • setTimeout
    • setInterval
    • setImmediate
    • 其他
  2. 微任务
    • Promise的then方法(注意Promise内部的内容是同步内容,立即执行)
    • process.nextTick,
    • MutationObserver

几个简单例子

定时器、Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setTimeout(function(){
console.log('定时器开始')
});

new Promise(function(resolve){
console.log('马上执行for循环');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数')
});

console.log('代码执行结束');

/*
* 马上开始for循环
* 代码执行结束
* 执行then函数
* 定时器开始
*/

解释:

  1. 执行到setTimeout,先由Event Table保留其回调函数。并且在其延迟任务完成后由事件触发线程将其加入到Event Queue
  2. 执行到new Promise,由于其函数参数是同步的,所以立即执行。打印:马上执行for循环。并将then方法的回调加入到微任务队列。
  3. 执行到 console.log('代码执行结束');,同步代码。直接打印:代码执行结束
  4. 第一个事件循环结束,到checkpoint,检查是否有微任务,发现then方法回调,直接打印:执行then函数
  5. 第二轮事件循环开始,执行第一个宏任务,setTimeout的回调进入执行栈,即执行 console.log('定时器开始')直接打印:定时器开始

定时器、Promise、async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

对于async函数,本质是基于Promise,所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
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');
})
}

这样就容易理解前面的答案了,根据Promise参数函数为同步,then方法为微任务,可以很容易推的结果。

最后

说实话,这一部分仍然没有很好的理解,主要是官方的文档解释的比较抽象并且没有定性的解释,加之我的英语不行,不能很好的理解整个流程,只能简单的确定执行次序。这一部分还需要在后面深入的理解。

参考

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :