浅谈Event Loop
从单线程说起
众所周知,js是一种单线程语言。为什么是单线程呢?我引用一句烂大街的话:假设js同时有两个线程,一个线程想要在某个dom节点上增加内容,另一个线程想要删除这个节点,这时要以哪个为准呢?当然,多线程有多线程的解决办法,加锁啊,但是这样的话,又会引入锁、状态同步等问题。
js是浏览器脚本语言,主要用途是与用户互动,操作dom,多线程会带来很复杂的同步问题。
好吧,那就单线程吧。但是单线程又带来了单线程的问题,只有一个线程啊,任务要排队执行,如果前一个任务执行时间很长(ajax请求后台数据),后面的任务就都得等着。
Event Loop就出现了,来背单线程的锅。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
宏任务&微任务
整理了一下常见了微任务、宏任务
- 常见的宏任务:setTimeout、setInterval、I/O、setImmedidate
- 常见的微任务:process.nextTick、MutationObserver、Promise.then、 catch finally、ajax请求
process.nextTick和setImmidate是只支持Node环境的。且process.nextTick是有一个插队操作的,就是说他进入微任务队列时,会插到除了process.nextTick 其他的微任务前面。
所以,我们上面提到的任务队列,是包括一个宏任务队列和一个微任务队列的。每次执行栈为空的时候,系统会优先处理微任务队列,处理完微任务队列里的所有任务,再去处理宏任务。
Event Loop
往下看之前你应该知道栈、队列、同步任务、异步任务(宏任务&微任务)、执行栈这些基本概念。
请看下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utRj5RTq-1588226339527)(images/event-loop.png)]
1、js在执行代码时,代码首先进入执行栈,代码中可能包含一些同步任务和异步任务。同步任务都在主线程(这里的主线程就是JS引擎线程)上执行。同步任务立即执行,执行完出栈,over。
2、异步任务会再分为宏任务和微任务。微任务会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。宏任务也会进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。
3、当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务。
以上三步会不断重复,这就是事件循环(Event Loop)。
demo
来看一个简单的demo。
setTimeout(function() {
console.log('1');
})
new Promise(function(resolve) {
resolve()
console.log('2');
}).then(function() {
console.log('3');
})
console.log('4');
//打印顺序 2 4 3 1
上面demo的图解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hgUHzlJg-1588226339546)(images/demo1.png)]
再看一个demo
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');
// 打印顺序是:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
看到async/await不必紧张,语法糖而已。async表示函数里有异步操作,await之前的代码该怎么执行怎么执行,await右侧表达式照常执行,后面的代码被阻塞掉,等待await的返回。返回是非promise对象时,执行后面的代码;返回promise对象时,等promise对象resolved时再执行。
所以可以理解成后面的代码放到了promise.then 里面。
- 输出 script start
- 之后把setTimeout里面的匿名回调函数丢进宏任务队列,简记为[‘setTimeout’]
- 输出async1 start
- 输出async2
- 要输出async1 end代码被丢进微任务队列,此时的微任务队列为[‘async1 end’]
- 输出promise1
- promise对象状态变为resolved
- promise.then 里的匿名函数进入微任务队列,此时的微任务队列为[‘async1 end’, ‘promise2’]
- 输出script end
- 执行栈空
- 输出async1 end
- 输出promise2
- 微任务队列为空
- 输出setTimeout
以上两个demo就是对Event Loop的一个练习,有哪些问题欢迎指正。