重读JavaScript-事件循环(Event Loop)

846 阅读5分钟

写在前面

一直想记录下自己在学习和工作中获得的经验,但苦于没有时间(其实就是懒)没有执行,这是自己的首篇技术文章,希望是个良好的开端(可以坚持下去)。同时感谢各位前辈的积累和分享,自己深知还有很多不足,还望大家可以提出自己的宝贵建议和意见,督促进步嘛。最后,希望分享的东西对你有那么一丢丢的帮助。

以上。

一些概念

  • JavaScript是单线程语言
  • Event Loop 是JavaScript的执行机制

事件循环

JavaScript是单线程语言,就像过安检一样,一个闸口只能一个人接着一个人过去,同理JavaScript任务也要一个一个按着顺序执行,但是如果有一个任务耗时过长,那后面的任务就要等待,这样就会造成大量的等待时间,这对于用户在访问网站时是不可接受的,那么如何解决这个问题呢?

聪明的设计者将任务分为两种

  • 同步任务
  • 异步任务
console.log(1)

console.log(2)

console.log(3)

// 1
// 2
// 3

JavaScript 自上向下执行,依次输出结果,此为同步任务。当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

以上的打印顺序是什么呢?

正确答案是:script start, script end, promise1, promise2, setTimeout

如何理解呢?先来下面这张非常关键的图

解读:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

从上图中我们了解了事件循环的基本原理,但其中有个问题需要说明下,就是我们什么时候知道主线程中的任务执行完毕了呢?

JavaScript引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

宏任务与微任务

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢?

执行机制:

  • 在执行栈中执行一个宏任务。
  • 执行过程中遇到微任务,将微任务添加到微任务队列中。
  • 当前宏任务执行完毕,立即执行微任务队列中的任务。
  • 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
  • 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。

一个掘金的老哥(ssssyoki)的文章摘要: 那么如此看来我给的答案还是对的。但是js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。 我当时看到这我就服了还有这种骚操作。

可以触发宏任务的操作有:

  • I/O
  • script
  • setTimeout
  • setInterval
  • setImmediate

可以触发微任务的操作有:

  • 原生Promise(有些实现的promise将then方法放到了宏任务中)
  • process.nextTick(node)
  • Object.observe(已废弃)
  • MutationObserver

下面我们用例子来说明下

setTimeout(()=>{
  console.log('setTimeout1')
},0)
let p = new Promise((resolve,reject)=>{
  console.log('Promise1')
  resolve()
})
p.then(()=>{
  console.log('Promise2')    
})

输出结果:Promise1,Promise2,setTimeout1

Promise参数中的Promise1是同步执行的 其次是因为Promise是microtasks,会在同步任务执行完后会去清空microtasks queues, 最后清空完微任务再去宏任务队列取值

Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})

setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)

输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一开始执行栈的同步任务执行完毕,会去 microtasks queues 找 清空 microtasks queues ,输出Promise1,同时会生成一个异步任务 setTimeout1
  • 去宏任务队列查看此时队列是 setTimeout1 在 setTimeout2 之前,因为setTimeout1执行栈一开始的时候就开始异步执行,所以输出 setTimeout1
  • 在执行setTimeout1时会生成Promise2的一个 microtasks ,放入 microtasks queues 中,接着又是一个循环,去清空 microtasks queues ,输出 Promise2
  • 清空完 microtasks queues ,就又会去宏任务队列取一个,这回取的是 setTimeout2

总结

其实,JavaScript的Event Loop简单来说,就是一段代码块开始自上而下执行,是同步任务优先开始执行,在遇到setTimeout等可以触发宏任务的操作时,先将这些宏任务放在宏任务queue中,等待执行,在遇到promise等可以触发微任务的操作时,将这些微任务放在微任务queue中,等待执行。

待同步任务(宏任务)执行完成后,优先执行微任务队列中的任务,待微任务队列中任务执行完毕后,开始执行下一个宏任务队列中的任务,如此循环,直到完成队列中任务,主线程没有待完成的任务时,此代码块执行完毕。

参考文章