大家好,我是蓝胖子的小叮当,从事前端也有几年了,之前一直在个人笔记上记录知识点和个人思考逻辑,后面会慢慢将知识点以文章形式发布上来,也希望大家多多关注,多多佐证。
1.1引言
在聊事件循环机制之前先了解一下前置知识
1.JS为什么是单线程的?
如果浏览器中的JS是多线程的,那么现在有两个进程process1、process2,当他们对同一个dom同时进行操作,process1删除了该dom,而process2编辑了该dom,同时下达两个冲突的命令,浏览器该如何执行呢?所以JS是单线程的。
2.JS为什么需要异步?
如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会阻塞,对于用户而言阻塞就相当于卡死,就导致了极差的用户体验,所以JS存在异步执行。
3.JS单线程又是如何实现异步的呢?
既然JS是单线程的,只能是通过事件循环(event loop),理解了事件循环机制,就理解了JS的执行机制。
1.2事件循环详解
看完前面的引言后是不是对事件循环机制的存在有些了解了呢,现在就来认识事件循环机制。
1.事件循环由以下三部分组成:
- 主线程:执行任务
- 任务队列:存放异步任务(微任务队列、宏任务队列)
- 任务轮循线程:检查任务队列中的异步任务,并将异步任务调入主线程执行
2.宏任务与微任务
任务之间是不平等的,有些任务对用户体验影响大,就应该优先执行,而有些任务属于背景任务,晚点执行没什么问题,所以设计了优先级队列的方式。
微任务就是得到优先执行的异步任务,宏任务就是优先级低的异步任务,所以任务队列包括宏任务队列和微任务队列
宏任务事件包括:
- script(整体代码)
- setTimeout
- setInterval
- I/O UI交互事件
- postMessage(不同源的文档进行通信)
- MessageChannel(跨页面通信)
微任务事件包括:
- Promise.then
- await后面的代码
- nextTick
- Object.observe(对任何对象的属性修改进行监视的事件处理函数)
- queueMicrotask(手动将一个函数添加到微任务队列中)
- MutationObserver(用来监视DOM变动的事件处理函数)
3.任务轮循
任务轮循既第一遍宏任务(主代码)执行结束,先执行所有微任务队列中的任务,再执行一个宏任务,以此类推直至宏任务队列和微任务队列都为空。总结:执行一个宏任务后就执行现有的所有微任务。
4.实战
async function async1 () {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise (function (resolve) {
console.log('promise1')
resolve();
}).then (function () {
console.log('promise2')
})
console.log('script end')
我们看上面这道面试题,按照上面说的知识点理一下逻辑,设queue1为宏任务队列,queue2为微任务队列
- 从头走一遍主代码,首先async1和async2函数申明但未执行,此时queue1为空,queue2为空
- 第11行没有任何异步直接执行,打印script start,此时queue1为空,queue2为空
- 第13行setTimeout定时器虽然是0ms,但他是宏任务,将第14行console.log('setTimeout')塞入queue1,此时queue1有1项任务,queue2为空
- 第17行执行async1函数,先直接打印async1 start,此时queue1有一项任务,queue2为空
- 第3行await触发async2,打印async2,await后面的代码是微任务,将第4行console.log('async1 end')塞入queue2,此时queue1有1项任务,queue2有1项任务
- 第19行Promise,先直接打印promise1,执行resolve()即触发promise.then,promise.then是微任务,将第23行console.log('promise2')塞入queue2,,此时queue1有1项任务,queue2有2项任务
- 第26行没有任何异步直接执行,打印script end
- 主代码走完,即走完一遍宏任务,现在去检查是否有微任务,发现有queue2内有两个任务,执行所有微任务,打印async1 end和promise2
- 微任务执行完,再检查是否存在宏任务,queue1有一个任务,执行queue1中的第一个任务,打印setTimeout
- 执行一个宏任务后再执行所有微任务,现在发现queue2为空,不用执行,再检查queue1是否有任务,queue1为空,该事件循环执行完毕
- 结果如下
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
1.3事件循环机制引起的问题
疑问.为什么定时器总是不准
定时器不准的情况,根据我们上面所讲,定时器的时间并不是执行函数的时间,而是最短n毫秒后将任务添加到队列中
也就是说,除非队列完全是空,否则定时器时间到了,他仅仅是开始排队罢了,有其他任务也在排队,所以定时器不可能完全准时
解决方案:
1.Web Worker(后续会出对应章节)他的作用就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程不会被阻塞或拖慢。
2.requestAnimationFrame写一个一秒的延时器代替setTimeOut,requestAnimationFrame是请求动画帧,它既不是宏任务也不是微任务,不会进入事件循环队列,而是在微任务执行完后浏览器的渲染机制,requestAnimationFrame会在渲染之前执行
好啦,事件循环机制就总结到这里,如果有什么疑问、意见或建议,大家都可畅所欲言,欢迎指教。