EventLoop是什么?
EventLoop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJs基于不同的技术实现了各自的EventLoop。
JS是单线程语言,JS的Event Loop是JS的执行机制。深入了解JS的执行,就等于深入了解JS里的event loop。
浏览器的Event Loop是在html5的规范中明确定义。
NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
JS中的EventLoop
(1)JS为什么是单线程的?
因为JS最初是被设计用在浏览器中,如果JS是多线程的,比如说:有两个线程process1和process2,由于是多线程的JS,两个线程同时对一个dom进行操作,process1删除了该dom,而process2修改了该dom,此时浏览器就没法执行了。所以说JS是单线程的。
(2)JS中为什么需要异步呢?
因为JS中代码自上而下执行,如果不存在异步,当一行代码执行的时间过长时,后边的代码就会被阻塞,对于用户而言,意味着页面卡死,体验极其不好。
(3)JS中如何实现异步的呢?
通过事件循环(EventLoop),理解了EventLoop机制,就理解了JS的执行机制。
(4)JS中的EventLoop
举个栗子1:观察下面代码的执行顺序
console.log(1)
setTimeout(funciton(){
console.log(2)
},0)
console.log(3) // 1 3 2
很显然,setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足一定条件后才执行的,这类代码就叫做异步代码。在JS中就是将任务分为同步任务和异步任务。
按照这种分类方式,JS的执行机制就是:
首先判断JS是同步还是异步代码,同步就进入主线程,异步就进入event table
异步任务在event table中注册函数,当满足触发条件后,被推入event queue
同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中
以上三步循环就是JS中的EventLoop。
举个栗子2:观察下面代码
setTimeout(function(){
console.log("定时器开始啦")
})
new Promise(function(resolve){
console.log("马上执行for循环啦")
for(var i = 0;i < 10000;i++){
i==99 && resolve()
}
}).then(function(){
console.log("执行then函数啦")
})
console.log("代码执行结束")
如果按照上面例1的结论来分析,就是:
setTimeout 是异步任务,被放到event table
new Promise 是同步任务,被放到主线程里,直接执行打印 console.log('马上执行for循环啦')
.then里的函数是 异步任务,被放到event table
console.log('代码执行结束')是同步代码,被放到主线程里,直接执行
所以结果是 【马上执行for循环啦 — 代码执行结束 — 定时器开始啦 — 执行then函数啦】吗?
然而执行后的结果居然不是这样,而是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】
那么,难道是异步任务的执行顺序,不是前后顺序,而是另有规定? 事实上,按照异步和同步的划分方式,并不准确。
更准确的划分方式是:
macro-task(宏任务):script,setTimeout,setInterval,setimmediate,requestAnimationFrame (浏览器独有),I/O,UI rendering(浏览器独有)
micro-task(微任务):promise,promise.nextTick,MutationObserver,Object.observe,其中promise.nextTick为node独有
那么按照这种分类方式,JS的执行机制就是
先执行一个宏任务,过程中如果遇到微任务就将其放入微任务的【事件队列】里
当前宏任务执行完毕后,会查看微任务的【事件队列】,并将队列里的微任务依次执行完
重复前面两步
再来分析下这个例子的执行顺序:
首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印"马上执行for循环啦"
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 "代码执行结束"
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"
关于setTimeout
对于这段代码:
setTimeout(function(){
console.log('开始执行')
},3000)
我们通常的理解是三秒后会打印“开始执行”。其实更准确的说法是:3秒后setTimeout里的函数会被推入event queue中,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。所以只有当满足3秒后并且主线程空闲时,才会在3秒后执行setTimeout里的函数。如果主线程的内容很多,执行事件超过了3秒,比如10秒,那么setTimeout里的函数只能在10秒后执行了。
浏览器的EventLoop
1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等)
2. 全局Script代码执行完毕后,调用栈Stack会清空
3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1
4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空
6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行
7. 执行完毕后,调用栈Stack为空
8. 重复第3-7个步骤
9. 重复第3-7个步骤
10. ……
需要注意的是:
1. 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务
2. 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空
3. UI rendering的节点,是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
举个栗子:
console.log(1)
setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
})
})
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data)
})
setTimeout(() => {
console.log(6)
})
console.log(7) //执行结果: 1 4 7 5 2 3 6