一 消息队列及事件循环
每个渲染进程都只有一个主线程(单线程)且任务繁忙,要处理 DOM、计算样式、处理布局、Javascript 任务及各种输入事件。为确保主线程有条不紊的运行,需要“消息队列”和“事件循环系统”来统筹调度这些任务。
消息队列是一种“先进先出”的数据结构,用来存放要执行的任务,从尾部添加,从头部取出。经过事件循环系统逐个取出并执行。
因为单线程按顺序逐个执行,前一个任务结束,才会执行后一个任务,这种情况下无法使高优先级的任务及时执行;如果前一个任务耗时很长,后一个任务就不得不一直等着,导致“阻塞”。解决这两个问题,需要使用到“异步回调函数”。
回调函数,将一个函数作为参数给另外一个函数,那作为参数的这个函数就是回调函数;分为同步回调和异步回调。
异步回调的两种方式:
- 异步回调函数封装成一个任务,添加到消息队列尾部(宏任务,如 setTimeout / XMLHttpRequest),当循环系统执行到该任务的时候执行回调函数。
- 以微任务形式体现:执行时机是在主函数执行结束之后、当前宏任务结束之前。
1.1 微任务处理高优先级任务
即,把高优先级的任务放到微任务队列中。
通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,比如,DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,等当前宏任务中的主要功能都完成之后,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
1.2 异步回调解决单个任务执行时长过久
通过回调的功能,让待执行的 Javascript 任务滞后执行。
二 宏任务与微任务
2.1 宏任务
页面中大部分任务都是在主线程上执行的,这些任务包括:渲染事件(如解析DOM、计算布局、绘制等);用户交互事件(如鼠标点击、滚动页面、放大缩小等);Javascript 脚本执行事件;网络请求完成事件;文件读写完成事件等。为使这些任务有条不紊的运行,会将这些任务放入“消息队列”中。
通常情况下,消息队列(包括延时队列)的任务都是“宏任务”。
宏任务中除了同步代码外,还包括微任务队列和下一个宏任务的回调。执行的顺序是:同步代码 > 微任务 > 宏任务回调。
2.2 微任务
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。微任务是 V8 引擎创建的。
微任务的产生方式:
- MutaionObserver(监控 DOM 节点)
- Promise (Async/Await)中的 then 回调
三 WebAPI: setTimeout / XMLHttpRequest
2.1 setTimeout
setTimeout 需要在指定的时间间隔内调用执行,而消息队列按顺序执行无法保证时间,所以定时器的回调函数不是放在消息队列中的,而是放在“延迟队列”,该队列专门用来维护定时器和 Chromium 内部需要延迟执行的任务。
setTimeout 的使用注意事项:
- 如果当前任务执行时间过久,会影响定时器任务的执行
- 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
- 未激活的页面,setTimeout 执行时间最小间隔是 1000 毫秒
- 延时执行时间有最大值:2147483647(24.8天)
- setTimeout 回调中的 this 指向全局环境或 undefined
2.2 XMLHttpRequest
XMLHttpRequest 的运作机制:XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。
四 Promise、async / await
Promise 函数本身的参数回调是同步的,.then()内的任务才会添加到微任务队列。
async 隐式返回 Promise 作为结果的函数,那么可以简单理解为,await 后面的函数执行完毕时,await 会产生一个微任务(Promise.then)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出 async 函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到 async 函数去执行剩下的代码,然后把 await 后面的代码注册到微任务队列当中。
五 输出顺序测试题
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
以上输出顺序为:script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout