一、为什么需要事件循环
- javascript 是单线程的编程语言,只有一个调用栈,在同一时间只能做一件事,按顺序来处理事件,但前端的某些任务是非常耗时的,比如网络请求、定时器和事件监听,如果让它们和别的任务一样,都老老实实排队等待执行的话,执行效率会非常低,甚至导致页面的假死。
- 在遇到耗时的任务(异步任务)时,js 并没有阻塞,还会继续执行,这就是因为有事件循环机制,实现了单线程非阻塞的方法。
二、javascript 编程语言
- javascript 是一种单线程的编程语言,同一时间只能做一件事。在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序进行。
- 在执行同步任务的时候,如果遇到异步任务,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。因此,js 又是一个非阻塞、异步、并发式的编程语言。
三、同步任务和异步任务
单线程意味着所有的任务都需要排队,前一个结束,才能执行后面的任务。如果队列是因为计算量大,CPU忙不过来,倒也算了,但是更多时候,CPU是闲置的,因为IO设备处理的很慢。例如,ajax读取网络请求,js设计者便想到,主线程可以完全不管IO设备,先将其挂起,然后执行后面的任务,等后面的任务结束掉,再反过来处理挂起的任务。
所有的任务分为以下两种:
1. 同步任务
- 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 所有同步任务都在主线程上执行,形成一个“执行栈”。
2. 异步任务
- 指的是不直接进入主线程,而进入“任务队列”(task queue)的任务,只有等主线程任务完毕,任务队列开始通知主线程,请求执行任务,该任务才会进入主线程执行。
- 主线程之外,还存在一个“任务队列”,只要异步任务有了运行结果,就在任务队列中放置一个事件。
- 一旦“执行栈”中的所用同步任务执行完毕,系统就会读取“任务队列”中的那些异步任务,进入执行栈,开始执行。
- 异步调用并不是要减少线程的开销,它的主要目的是让调用方法的主线程不需要同步等待在这个函数调用上,避免页面假死的现象,从而让主线程继续执行它下面的代码,提高效率。
3. 图解
四、宏任务和微任务
1. 宏任务
1)宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。
2)常见的宏任务有:
- script(外层的同步代码)
- setTimeout / setInterval
- UI rendering / UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
2. 微任务
1)一个需要异步执行的函数,执行时机是在主函数执行结束后,当前宏任务结束之前。
2)常见的微任务有:
- Promise.then(promise本身是同步的,它的.then方法是异步的)
- MutationObserver
- process.nextTick(Node.js)
3. 两种任务的执行时机
- 先执行宏任务,将宏任务放入任务队列,然后再执行微任务,将微任务放入微任务队列。但这两个队列不是一个队列,当往外拿的时候先从微任务队列里拿这个回调函数,然后再从宏任务的队列里拿宏任务的回调函数。
五、事件循环
1. 简单理解
- 同步和异步任务分别进入不同的“场所”,同步进入主线程,异步进入 Event Table 并注册函数。当指定的事件完成时,Event Table 会将这个函数移入到任务队列(Event Queue)。主线程内的任务执行完毕为空时,就去任务队列中读取对应的函数,进入主线程执行。上述过程会不断重复,也就形成了事件循环(Event Loop)。
2. 执行顺序
1)例子一:
console.log('---start---') // 同步任务
setTimeout(() => {
console.log('setTimeout') // 宏任务
}, 0)
new Promise((resolve, reject) => {
console.log('promise') // 同步任务
resolve()
}).then(() => {
console.log('then') // 微任务
})
console.log('---end---') // 同步任务
// 执行结果:
// ---start---
// promise
// ---end---
// then
// setTimeout
2)例子二:
async function async1() {
console.log('async1') // 同步
await async2()
console.log('await1') // 阻塞(await会阻塞下面的代码,即加入微任务队列)
}
async function async2() {
console.log('async2') // 同步
}
console.log('---start---') // 同步
setTimeout(() => {
console.log('setTimeout') // 宏任务
}, 0)
async1()
new Promise((resolve, reject) => {
console.log('promise') // 同步
resolve()
}).then(() => {
console.log('then') // 微任务
})
console.log('---end---') // 同步
// 执行顺序
// ---start---
// async1
// async2
// promise
// ---end---
// await1
// then
// setTimeout
- Promise 的特点是无等待,会立即执行,它本身是同步任务,但是. then 方法是异步任务。
- async 函数会返回一个 promise 对象,因为 Promise 会立即执行,所以 async 函数在没有 await 的情况下执行,则会立即执行。
- 当 async 函数执行到 await 时,会将 await 后面接的函数先执行一遍,然后跳出这个 async 函数,继续执行,同步任务执行完毕后,回到 async 函数中,如果这个 await 函数已经有了结果,那么就继续执 async函数,否则继续等待。
六、小总结
javaScript 是一门单线程语言,异步操作都是放在事件循环队列里面,等待主线程中的执行栈来执行的,并没有专门的异步执行线程。