从上图可以看出:
渲染主线程相当于一个 for 循环,不断读取消息队列中的任务并执行。
IO 线程通过 IPC 与其他进程通信,接受其他进程传过来的任务并添加到消息队列中。
除了图上画的之外,渲染进程中还有预解析线程、合成线程等往消息队列中添加任务。
浏览器页面是由消息队列和事件循环系统来驱动的。当循环系统执行一个任务的时候,都要为这个任务维护一个系统调用栈,通过 perfomance 面板可以看到系统调用栈的记录。
如何保证主线程能够安全退出?
设置一个 boolean 退出标志,主线程循环读取执行任务时,若发现标志为 true, 则直接退出线程循环。
消息队列中的任务类型
- 内部类型:如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JS 定时器等。
- 与页面相关的事件:JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。
这些事件都在主线程中执行,在编写代码时,要衡量这些事件所占用的时长,并想办法解决单个任务占用线程过久的问题。
页面使用单线程的缺点
单线程,即每次需要等待上一个任务执行完成,才能执行下一个。
就会带来以下问题:
如何处理高优先级的任务?
最常见的就是监控 DOM 变化。
如果用 JS 设计一套监听接口,当 DOM 频繁变化时,当前任务的执行时间就会被拉长。
如果将 DOM 变化当作异步的消息事件添加到队列的尾部,就会影响监控的实时性。
针对这种情况,就推出了微任务。
消息队列中的任务,我们称之为宏任务。每个宏任务中都包含一个微任务队列。如果 DOM 有变化,就将其添加到微任务队列中。等宏任务中的主要事件执行完后,就会执行当前宏任务中的微任务队列中的任务,而不是执行下个宏任务。
如何解决单个任务执行时长过久的问题?
因为所有任务都在单线程中执行,如果一个执行时间过长就会影响后面的任务,从而造成卡顿。
解决方法:使用回调功能。
setTimeout 实现机制
Chrome 中除了正常使用的消息队列外,还有一个延迟队列。当 JS 创建定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。
延迟队列中的任务会在消息队列的一个任务执行完成之后执行。
- 延迟队列的添加和执行伪代码:
// 声明延迟队列DelayedIncomingQueue delayed_incoming_queue;struct DelayTask { ... }DelayTask timeTask;timerTask.cbf = showName; // 将回调函数赋给延迟队列中任务的 cbf 属性timerTask.startTime = getCurrentTime() // 获取当前时间timerTask.delayTime = 200 // 设置延迟时间delayed_incoming_queue.push(timerTask) // 将延迟任务添加到延迟队列void MainThread() {for(;;) {// 执行消息队列中的任务ProcessTask(task_queue.takeTask())// 执行延迟队列中的任务ProcessDelayTask()// 若设置了退出标志,直接退出线程循环if (!keep_running) {break;}}}
延迟队列实际上是一个 hashmap 结构,等到执行这个结构的时候,会计算 hashmap 中的每个任务是否到期,到期了就去执行,直到所有到期任务都执行结束,才会进入下一轮循环。
使用 setTimeout 注意事项
- 从上面伪代码可以看出,如果当前任务执行过久,会影响延迟队列中任务的执行。
- 若 setTimeout 存在签套调用,则系统会设置最短时间间隔为 4ms.
function cb() {setTimeout(cb, 0);}setTimeout(cb, 0);

从上图可以看出,前 5 次调用 setTimeout 的时间间隔较小,后面每次调用最小间隔是 4ms.所以,一些实时性较高的需求,比如动画就不能用 setTimeout 来实现了。
- 未激活的页面,setTimeout 执行的最小间隔是 1000ms
如果当前的 tab 页没有被激活,那么定时器的最小间隔会被设置为 1000ms,目的是优化后台页面加载损耗及降低耗电量。 - 延时执行时间有最大值。
Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行。
function showName() {console.log("your name");}setTimeout(showName, 2147483648);
- setTimeout 中的 this 不符合直觉
回调
每个任务在执行过程中都有自己的系统调用栈,同步回调就是当前主函数执行上下文中的回调函数;异步函数的回调则是在主函数之外执行,有两种方式:
- 把异步函数做成一个任务,添加到队列尾部
- 把异步函数添加到微任务队列,在当前任务末尾执行
同步回调
let cb = () => console.log('doing work')function doWork(cb) {console.log('start)cb()console.log('end')}doWork(cb)
异步回调
let cb = () => console.log('doing work')function doWork(cb) {console.log('start)setTimeout(cb, 0)console.log('end')}doWork(cb)
XMLHttpRequest 运作机制

完整流程
function getData(url) {// 1. 创建请求对象let xhr = new XMLHttpRequest()// 2. 注册相关事件回调处理函数xhr.onreadystatechange = () => { ... }xhr.ontimeout = (e) => { ... }xhr.onerror = (e) => { ... }// 3. 打开请求xhr.open('GET', url, true) // true 为采用异步// 4. 配置参数xhr.timeout = 3000 // 设置超时时间xhr.responseType = 'text' // 设置响应的数据格式 text,json,document,blob,arraybufferxhr.setRequestHeader('x-test', 'wulala') // 自定义请求头// 5. 发送请求xhr.send()}
注意问题
- 跨域问题
- HTTPS 混合内容问题(https 下请求 http 协议的图片、内容等)
宏任务
通常我们把从消息队列中取出的任务称为宏任务。
包括以下几种事件:
渲染事件:如解析 DOM、计算布局、绘制;
用户交互事件:如鼠标点击、滚动页面、放大缩小等;
JS 脚本执行事件;
网络请求完成、文件读写完成事件。
微任务
微任务是一个需要异步执行的函数,执行时机在主函数执行结束后、当前宏任务结束之前。
在执行一段 JS 脚本是,V8 会为其创建一个全局执行上下文,同时会在内部创建一个微任务队列,用来存放当前宏任务产生的微任务。
为什么会有微任务?
<!DOCTYPE html><html lang="en"><head>...</head><body>...<script>function timerCallback2() {console.log(2);}function timerCallback() {console.log(1);setTimeout(timerCallback2, 0)}setTimeout(timerCallback, 0)</script></body></html>

从上图可以看到,宏任务 settimeout 的时间粒度大,执行的时间间隔不可控。对一些高实时性的需求就不符合。
如何产生微任务?
现代浏览器的两种产生方式:
- 使用 MutationObserver 监控 DOM 节点,然后通过 JS 来修改这个节点或添加、删除子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
- 使用 Promise,当调用
Promise.resolve()或Promise.reject()时,也会产生微任务。
如何执行微任务?
在 JS 引擎准备退出全局执行上下文并清空调用栈的时候,JS 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
如果在执行微任务队列的过程中产生新的微任务,也会添加到本次宏任务的微任务队列中,知道队列为空才算执行结束。

监听 DOM 变化方法演变
- 通过setTimeout, setInterval 轮询检测页面 DOM 是否改变。
缺点:
浪费很多无用功去检查 DOM,低效;
间隔不好把控,DOM 变化响应不及时。 - 使用 Mutation Event 观察者模式,当 DOM 有变动立刻触发,属于同步回调。
缺点:
每次 DOM 变动,渲染引擎都会调 JS,性能开销大,易卡顿。 - 使用 MutationObserver 将响应函数改为异步调用,等多次 DOM 变化后,一次触发异步调用,并且还有一个数据结构来记录这期间所有 DOM 的变化。
- 解决了性能问题,如何保证消息通知的及时性呢?
微任务,在 DOM 节点发生变化时,渲染引擎将变化记录封装成微任务加入微任务队列。
综上所述,MutatuonObserver 采用了 “异步 + 微任务”。
异步:解决性能问题
微任务:解决实时性问题
精选问答
- 宏任务与微任务
宏任务是开会分配的工作内容,微任务是工作过程中被临时安排的内容。 - requestAnimationFrame 实现动画效果为什么比 setTimeout 好?
使用 requestAnimationFrame 不需要设置具体的时间,由系统来决定回调函数的执行时间,requestAnimationFrame 里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次,内如果页面未激活的话,requestAnimationFrame 也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销。
若 js 脚本阻塞页面刷新,requestAnimationFrame 也不一定确保 16ms 执行一次。 - IPC 是什么?
进程间通信,比如浏览器进程需要网络进程下载数据,浏览器进程就是通过 IPC 告诉网络进程需要下载哪些数据,网络进程接收到之后才会开启下载流程。

