浏览器线程
JavaScript是单线程的,这里的单线程指的是:浏览器中负责解释和执行JavaScript代码的只有一个线程,也就是JS引擎线程,但是浏览器的渲染进程是提供多个线程的:
1. GUI渲染线程
- 负责渲染浏览器界面,解析HTML、CSS、构建DOM树和RenderObject树、布局和绘制等;
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行;
- 注意:GUI渲染线程和JS引擎线程是互斥的,JS引擎线程优先级高于GUI渲染线程,当JS引擎执行时GUI线程会被挂起,GUI更新会保存在一个队列中等到JS引擎空闲时间立即被执行。
2. JS引擎线程
- 也成为JS内核,负责处理和解析JavaScript脚本程序,运行代码,如V8引擎;
- JS引擎一直等待这任务队列中的任务,然后加以处理,render进程中永远只有一个JS线程在运行js程序
- 同样注意:GUI渲染线程和JS引擎线程是互斥的,JS执行时间过长,就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3. 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环;
- 当JS引擎执行代码块如:setTimeout、鼠标点击、Ajax异步请求,会将对应的任务添加到事件线程中;
- 当对应的时间服务触发条件时,该线程会把事件添加到待处理队列的队尾,等到JS引擎来处理。
4. 定时处理线程
- setInterval与setTimeout所在的线程;
- 浏览器定时计数器并不是有JavaScript引擎计数的,它是通过单独线程来计时并触发定时;
- 注意:W3C在HTML标准中规定,要求setTimeout中低于4ms的时间间隔算为4ms。
5. 异步http请求线程
- 在XMLHttpRequest在连接后通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就会产生状态变更事件,将这个回调再放入事件队列中,再由JavaScript引擎执行。
浏览器渲染流程
这里从浏览器内核拿到内容开始,大概经历如下几个步骤:
- 解析html,建立dom树
- 解析css,构建render树,将css代码解析树型的数据结构,然后结合DOM合并成render树
- 布局render树(Layout和reflow),负责各元素尺寸和位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上
注意:具体流程及优化方式将在前端性能优化篇章进行详细讲解。
事件循环
事件循环机制和异步事件队列(消息队列)的维护是由事件触发线程控制的。
事件触发线程同样是浏览器渲染引擎提供的,它会维护一个异步事件队列。
JS引擎线程遇到异步,就会交给相应的线程单独去维护异步任务,等异步任务执行完成,然后由事件触发线程将异步对应的回调函数加入到异步事件队列中。
JS引擎线程会维护一个同步执行栈,同步代码会依次加入执行栈进行执行,执行结束后出栈。如果执行栈中的任务执行完成,即同步执行栈空闲,事件触发线程就会从异步事件队列中取出最先加入的异步回调函数进行执行,提取规则遵循先入先出FIFO规则,异步事件队列类似队列的数据结构。
微任务宏任务
所有任务分为微任务microtask和宏任务macrotask。
macrotask:
- 主代码块
- setTimeout
- setInterval
- I/O(ajax)
- UI rendering
- setImmediate(nodejs)
- 可以看到,事件队列中的每一个事件都是一个 macrotask,现在称之为宏任务队列
microtask:
- Promise
- Object.observe(已经废弃)
- MutationObserver
- process.nextTick(nodejs)
先看一张图:
- 执行一个宏任务(栈中没有就从事件队列中获取,可以理解为一个script标签)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
注: 宏任务是js引擎进行处理的,微任务是浏览器的行为。微任务必然是由宏任务执行是创建的。
例子
<script>
console.log("1-start");
setTimeout(() => {
console.log("1-setTimeout");
}, 0);
new Promise((resolve, reject) => {
console.log("1-Promise");
resolve();
}).then(() => {
console.log("1-Promise.then");
});
console.log("1-end");
</script>
<script>
console.log("2-start");
setTimeout(() => {
console.log("2-setTimeout");
}, 0);
new Promise((resolve, reject) => {
console.log("2-Promise");
resolve();
}).then(() => {
console.log("2-Promise.then");
});
console.log("2-end");
</script>
// 注意中间连个script标签,有和没有的区别
// 有:输出如下
1 - start;
1 - Promise;
1 - end;
1 - Promise.then;
2 - start;
2 - Promise;
2 - end;
2 - Promise.then;
1 - setTimeout;
2 - setTimeout;
// 无 输出如下
1 - start;
1 - Promise;
1 - end;
2 - start;
2 - Promise;
2 - end;
1 - Promise.then;
2 - Promise.then;
1 - setTimeout;
2 - setTimeout;