目录
例子3 settimeout, async, promise混用时
浏览器内核分为渲染引擎和JS引擎 | 浏览器多进程、浏览器内核多线程
全文概要
- JS引擎是单线程,单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
- 任务有同步任务和异步任务。任务又可分为: macrotasks 和 microtasks。
- 主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的js这种运行机制又称为Event Loop(事件循环)。队列是一种先进先出的数据结构。
- 浏览器内核的各个线程执行相应的异步任务,并把回调结果放入任务队列中。JS的事件循环机制基于浏览器渲染进程的事件触发线程等。
- 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
-
浏览器在遇到<script>标签时,DOM构建将暂停,直至脚本完成执行,然后继续构建DOM。如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。现在可以在script标签上增加属性defer或者async。脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM树和CSSOM规则树上。
-
整体流程,梳理这个过程中最为主要的三个进程,浏览器进程 网络进程 渲染进程,它们各自的职责以及三者之间的通信
宏任务和微任务
基本概念
macrotasks: script(整体代码), setTimeout
, setInterval
, setImmediate
, I/O, UI rendering
microtasks: process.nextTick
, Promises
, Object.observe
(废弃), MutationObserver
执行过程
注意理解:Promise的队列与setTimeout的队列的不同。
JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。
setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。
在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
JavaScript引擎首先从macrotask queue中取出第一个任务, 执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行; 然后再从macrotask queue中取下一个, 执行完毕后,再次将microtask queue中的全部取出;循环往复,直到两个queue中的任务都取完。
- 异步任务是由浏览器执行的,不管是
AJAX
请求,还是setTimeout
等 API,浏览器内核会在其它线程中执行这些操作,当操作完成后,将操作结果以及事先定义的回调函数放入 JavaScript 主线程的任务队列中 - JavaScript 主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环
- 当主线程阻塞时,任务队列仍然是能够被推入任务的。这也就是为什么当页面的 JavaScript 进程阻塞时,我们触发的点击等事件,会在进程恢复后依次执行。
例子1
console.log('stack one');
setTimeout(() => {
console.log('macrotasks start');
}, 0);
Promise.resolve()
.then(() => console.log('microtasks start'))
.catch(e => console.log(e));
console.log('stack two');
打印如图所示
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
例子2
// 宏任务1
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任务1
console.log('micro task 1');
setTimeout(() => {
// 宏任务3
console.log('macro task 3');
}, 0)
})
setTimeout(() => {
// 宏任务2
console.log('macro task 2');
}, 1000)
console.log('========== Sync queue(macro task 1) ==========');
打印:
========== Sync queue(macro task 1) ==========
micro task 1
macro task 3
macro task 2
如果把setTimeout(() => { // 宏任务2 console.log('macro task 2'); }, 1000)改为立即执行setTimeout(() => { // 宏任务2 console.log('macro task 2'); }, 0),
那么它会在macro task 3之前执行,因为定时器是过多少毫秒之后才会加到事件队列里
例子3 settimeout, async, promise混用时
async函数会返回一个Promise对象,且Promise对象是立即执行的。
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
},0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
我在webkit内核的浏览器上的运行结果如图所示。
上面这个例子,详细的请看这个博主的解说:https://blog.csdn.net/weixin_43606158/article/details/91360230
我从宏任务微任务的角度补充几点:
- async2()返回一个Promise对象,会立即执行;“console.log('promise1')”是同步任务。
- await后面的语句等同于Promise.resolve(),进入promise的异步队列排队。所以async1 end要在script end之后执行。
- 先把微任务的promise().then()执行完,再执行宏任务的setTimeout()。
参考
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
https://juejin.im/entry/58d4df3b5c497d0057eb99ff
https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810
https://www.cnblogs.com/jiangyuzhen/p/11064408.html 【建议把这个博客的例子全都理解】
Microtask 的应用
为啥要用 microtask?根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。
根据我们上面提到的事件循环进程模型,每一次执行 task 后,然后执行 microtasks queue,最后进行页面更新。如果我们使用 task 来设置 DOM 更新,那么效率会更低。而 microtask 则会在页面更新之前完成数据更新,会得到更高的效率。
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
即:(`task->渲染->task->...`)
为什么JS是单线程的
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
浏览器内核分为渲染引擎和JS引擎 | 浏览器多进程、浏览器内核多线程
以下摘抄自:https://segmentfault.com/a/1190000012925872#item-1 (教科书级讲解)
渲染引擎负责对html, css的解释并渲染网页,决定了浏览器如何显示网页的内容以及页面的格式信息。同一网页在不同内核的浏览器的渲染效果,会因为渲染引擎度编写语法的解释的不同而不同。常见的渲染引擎如webkit引擎:Chrome,Safari,Opera;tredent引擎:IE;等等。
JS引擎负责对javascript进行解释,编译和执行,以使网页达到一些动态效果。常见的JS引擎如google V8,微软,等等。
在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)
浏览器的渲染进程是多线程的
浏览器都包含哪些进程
主要进程为:
- Browser进程:浏览器的主进程(负责协调、主控),只有一个。
- GPU进程:最多一个,用于3D绘制等。
- 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为:页面渲染,脚本执行,事件处理等。页面的渲染,JS的执行,事件的循环,都在这个进程内进行。以下重点分析这个进程。
浏览器的浏览器渲染进程包含哪些线程
GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程(JS事件循环机制就是基于
事件触发线程
的)
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
定时触发器线程:
setInterval
与setTimeout
所在线程异步http请求线程:在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
浏览器渲染的步骤
浏览器器内核拿到内容后,开始渲染。渲染完毕后就是load
事件了,之后就是自己的JS逻辑处理了。渲染大概可以划分成以下几个步骤:
- 解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息 (计算每个节点的几何信息)
- 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。(将各个节点绘制到屏幕上)
展开:Reflow, Repaint。reflow成本比repaint高得多。