JavaScript基础(一)事件循环

165 阅读5分钟

大家好,我是蓝胖子的小叮当,从事前端也有几年了,之前一直在个人笔记上记录知识点和个人思考逻辑,后面会慢慢将知识点以文章形式发布上来,也希望大家多多关注,多多佐证。

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.任务轮循

任务轮循既第一遍宏任务(主代码)执行结束,先执行所有微任务队列中的任务,再执行一个宏任务,以此类推直至宏任务队列和微任务队列都为空。总结:执行一个宏任务后就执行现有的所有微任务。

image.png

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为微任务队列

  1. 从头走一遍主代码,首先async1和async2函数申明但未执行,此时queue1为空,queue2为空
  2. 第11行没有任何异步直接执行,打印script start,此时queue1为空,queue2为空
  3. 第13行setTimeout定时器虽然是0ms,但他是宏任务,将第14行console.log('setTimeout')塞入queue1,此时queue1有1项任务,queue2为空
  4. 第17行执行async1函数,先直接打印async1 start,此时queue1有一项任务,queue2为空
  5. 第3行await触发async2,打印async2,await后面的代码是微任务,将第4行console.log('async1 end')塞入queue2,此时queue1有1项任务,queue2有1项任务
  6. 第19行Promise,先直接打印promise1,执行resolve()即触发promise.then,promise.then是微任务,将第23行console.log('promise2')塞入queue2,,此时queue1有1项任务,queue2有2项任务
  7. 第26行没有任何异步直接执行,打印script end
  8. 主代码走完,即走完一遍宏任务,现在去检查是否有微任务,发现有queue2内有两个任务,执行所有微任务,打印async1 end和promise2
  9. 微任务执行完,再检查是否存在宏任务,queue1有一个任务,执行queue1中的第一个任务,打印setTimeout
  10. 执行一个宏任务后再执行所有微任务,现在发现queue2为空,不用执行,再检查queue1是否有任务,queue1为空,该事件循环执行完毕
  11. 结果如下
// 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会在渲染之前执行

好啦,事件循环机制就总结到这里,如果有什么疑问、意见或建议,大家都可畅所欲言,欢迎指教。