【深度剖析】曾经让人无法理解的事件循环

事件循环

在浏览器端,JS 是单线程的,也就是说,在同一个时刻最多只有一个代码片段在执行,可是浏览器又可以很好的处理异步请求,到底是为什么呢?

先来说明执行中的两个线程:

  • 主线程:JS 引擎执行的线程,只有一个,负责页面渲染、函数处理。
  • 工作线程:也称为幕后线程,这个线程可能存在于浏览器或 JS 引擎内部,与主线程是分开的,处理文件读取、网络请求等异步事件。

在主线程中有一个执行栈,所有的 JS 代码都会在执行栈里运行。在执行代码的过程中,如果遇到一些异步代码,比如,setTimeoutajax 等等,那么浏览器就会将这些代码放到工作线程中执行,在前端由浏览器底层执行,这个线程的执行不会阻塞主线程的执行,主线程继续执行栈中的剩余代码。

当工作线程里的代码执行完成后,该线程就会将它的回调函数放到任务队列中(又称为事件队列,消息队列)等待执行。而当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务的到来,因此,这也被称为事件循环。

任务队列

从上面可知,工作线程会将异步的回调函数放到任务队列中,然后让主线程来执行,那么问题来了,如果任务队列中有多个任务,那么要执行哪个呢?

JS 中,有两个任务队列,一个叫做 Macrotask Queue(Task Queue) 大任务,另一个是 Microtask Queue 小任务。

Macrotask 常见的任务:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • 用户交互操作,UI 渲染

Microtask 常见的任务:

  • Promise
  • process.nextTick
  • Object.observe

如果两种任务同时出现,事件循环执行是这样的:

  1. 检查大任务队列是否为空,若不为空,则进行下一步,若为空,跳到3
  2. 从大任务队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完进入下一步
  3. 检查小任务队列是否为空,若不为空,则进行下一步,否则跳到1
  4. 从小任务队列中取出队首(在队列时间最长)的任务进去事件队列

简而言之,一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,立即执行 Microtask 队列中的所有任务。

基于这个结论来看一个例子:

console.log(1)
setTimeout(function() {
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
    console.log(12)
  })
  .then(function() {
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);
//promise2
Promise.resolve()
  .then(function() {
    console.log(7)
  })
  .then(function() {
    console.log(8)
  })
console.log(9)

由上面的理论,一旦遇到异步代码则交给工作线程处理,主线程继续往下执行,所以首先会打印 1 和 9.

接着,执行 Microtask 中的所有任务,所以依次打印 7、8、2 (因为主线程也属于一个 Macrotask

最后,剩余的都是 Macrotask 任务了,所以就依次执行,输出 3、10、11、12、13.

定时器准时吗?

由上面的事件循环机制,引申出一个问题:定时器的时间准确吗?比如,setTimeout(func,100) 真的在 100 毫秒以后执行吗?

答案很遗憾,不是的。

const s = new Date().getSeconds();

setTimeout(function() {
 // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
 console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
 if(new Date().getSeconds() - s >= 2) {
     console.log("Good, looped for 2 seconds");
     break;
 }
}

在上面的循环里面,这是一个耗时的操作,这里大概耗费了2秒,所以在2秒以后才会执行 setTimeout() ,这里的 500 毫秒其实是指在 500 毫秒以后进入 Macrotask ,但不意味着立马回被执行,原因就是当前主线程在进行一个耗时的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值