事件循环
在浏览器端,JS
是单线程的,也就是说,在同一个时刻最多只有一个代码片段在执行,可是浏览器又可以很好的处理异步请求,到底是为什么呢?
先来说明执行中的两个线程:
- 主线程:
JS
引擎执行的线程,只有一个,负责页面渲染、函数处理。 - 工作线程:也称为幕后线程,这个线程可能存在于浏览器或
JS
引擎内部,与主线程是分开的,处理文件读取、网络请求等异步事件。
在主线程中有一个执行栈,所有的 JS
代码都会在执行栈里运行。在执行代码的过程中,如果遇到一些异步代码,比如,setTimeout
,ajax
等等,那么浏览器就会将这些代码放到工作线程中执行,在前端由浏览器底层执行,这个线程的执行不会阻塞主线程的执行,主线程继续执行栈中的剩余代码。
当工作线程里的代码执行完成后,该线程就会将它的回调函数放到任务队列中(又称为事件队列,消息队列)等待执行。而当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务的到来,因此,这也被称为事件循环。
任务队列
从上面可知,工作线程会将异步的回调函数放到任务队列中,然后让主线程来执行,那么问题来了,如果任务队列中有多个任务,那么要执行哪个呢?
在 JS
中,有两个任务队列,一个叫做 Macrotask Queue(Task Queue)
大任务,另一个是 Microtask Queue
小任务。
Macrotask
常见的任务:
setTimeout
setInterval
setImmediate
I/O
- 用户交互操作,
UI
渲染
Microtask
常见的任务:
Promise
process.nextTick
Object.observe
如果两种任务同时出现,事件循环执行是这样的:
- 检查大任务队列是否为空,若不为空,则进行下一步,若为空,跳到3
- 从大任务队列中取队首(在队列时间最长)的任务进去执行栈中执行(仅仅一个),执行完进入下一步
- 检查小任务队列是否为空,若不为空,则进行下一步,否则跳到1
- 从小任务队列中取出队首(在队列时间最长)的任务进去事件队列
简而言之,一次事件循环只执行处于 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
,但不意味着立马回被执行,原因就是当前主线程在进行一个耗时的操作。