异步与Event Loop
# 浏览器是多进程的
它主要包括以下进程
- Browser进程:浏览器的主进程,负责创建和销毁其他进程、网络资源的下载与管理、浏览器界面的展示、前进后退等。
- GPU进程:用于3D绘制等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- 浏览器渲染进程(浏览器内核):渲染进程内部是多线程的,每打开一个网页就会创建一个进程,主要用于页面渲染,脚本执行,事件处理等
渲染进程
浏览器的渲染的多线程的,页面的渲染,JavaScript的执行,事件的循环,都在这个进程内进行
- GUI渲染线程:负责渲染浏览器界面
- JavaScript引擎线程:负责处理JavaScript脚本程序、解析JavaScript运行代码等
- 事件触发线程:用来控制浏览器事件循环
- 定时触发器线程:
setInterval
与setTimeout
所在线程 - 异步http请求线程:在
XMLHttpRequest
连接后通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。
其中GUI渲染线程与JS引擎线程是互斥的,由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了。
JavaScript阻塞页面加载
因为JS引擎与GUI渲染进程是互斥的,所以如果GUI需要进行更新的同时,JS引擎正在进行巨量的计算,那么GUI渲染进程需要等待JS引擎空闲后才能执行,如此一来便会导致页面的渲染不连贯。
浏览器渲染流程
浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:
- 解析HTML,建立DOM树
- 解析CSS,将CSS代码解析成树形结构然后结合DOM合并成render树
- 布局render树,计算元素的尺寸或位置
- 绘制render树,绘制页面像素信息
- 浏览器将各层信息给GPU,GPU将各层合成显示在屏幕上
# JavaScript是单线程的
所谓单线程,是指在 JavaScript 引擎中负责解释和执行 JavaScript 代码的线程唯一,同一时间上只能执行一件任务。
JavaScript单线程机制的原型是为了避免DOM渲染冲突。如果 JavaScript 引擎线程不是单线程的,那么可以同时执行多段 JavaScript,如果这多段 JavaScript 都修改 DOM,那么就会出现 DOM 冲突。
同步与异步
如果在函数返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
let a = 1
Math.floor(a)
console.log(a) // 1
2
3
如果在函数 func
返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
fs.readFile('foo.txt', 'utf8', function(err, data) {
console.log(data);
});
2
3
JavaScript采用异步编程的原因是
- JavaScript是单线程的
- 提高CPU的利用率
# Event Loop
我们知道JavaScript是单线程的,那么这样势必会发生因异步请求时间过长而导致阻塞的问题。解决此问题的方式就是事件循环。
# 执行栈与事件队列
我们知道,当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数和这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈(Call Stack),JavaScript中只有一个执行栈。
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。
如果当前执行的代码是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。
如果就是引擎遇到一个异步事件之后,他会将这个事件挂起,继续调用执行栈中的其他任务。当该异步事件执行结束之后,js会将这个事件加入与当前执行栈不同的另一个队列,事件队列(Task Queue),这也被叫做任务队列或消息队列。
被放入事件队列后不会立刻执行其中的回调函数,而是等待执行栈中所有的任务执行完毕,主线程处于闲置状态时,主线程回去事件队列中查询是否有任务。如果有,那么主线程会依次取出其中的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码,执行栈为空之后再检查事件队列。这样就形成了一个无限的循环。
# macrotask与microtask
在事件循环中,异步事件会被放入事件队列中。然而,浏览器会根据这个异步事件的类型,把这个事件放入对应的宏任务队列或者微任务队列中去。
所以异步任务被分为两类:微任务(micro task)和宏任务(macro task)。它们的执行优先级是有区别的。
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
return console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
// 1 5 3 4 2
2
3
4
5
6
7
8
9
10
11
12
13
14
宏任务
可以理解是每次执行栈执行的代码就是一个宏任务。浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染
宏任务主要包括
- setTimeout
- setInterval
- setImmediate
- I/O
- UI交互事件
微任务
微任务可以理解是在当前宏任务执行结束后立即执行的任务,也就是说在某一个宏任务执行完后,就会将在它执行期间产生的所有微任务都执行完毕
微任务主要包含
- Promise
- process.nextTick
- MutaionObserver
异步任务运行机制如下
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
# TODO
https://github.com/FridaS/blog/issues/22
参考资料