先来看一张图片

主线程调用栈
所谓的JS单线程是指一个浏览器的主进程中只有一个JS执行线程。JS的执行线程的调用栈(stack)都是同步的(因为是单线程,所以一次只能做一件事),JS执行线程的主线程上的所有同步的任务都会放在这个执行栈中。
console.log('event loop1');
console.log('event loop2');
简单如以上代码,就被放在Call Stack执行栈中顺序执行,然后遵循先进后出,后进先出的原则销毁。输出的顺序是:先是event loop1后是 event loop2。
事件循环机制
如果执行过程中遇到DOM操作、ajax或setTimeout等异步操作的时候,会提交给相应的异步进程处理。
如:DOM操作是浏览器内核的DOM binding模块处理;Ajax是浏览器内核的Network模块处理;setTimeout是由浏览器内核的Timer模块处理。
处理完的回调就被推入到任务队列callback queue(应该叫event queue可能更准确些)。等待主线程的调用栈空闲。
一旦主线程的task执行完毕后,浏览器执行环境的Event Loop机制就查询任务队列,把队列里排在前面的回调压入主调用栈执行,如此重复。这个过程就称为事件循环。
所以我们就可以很容易理解以下代码:
console.log('hello');
setTimeout(function(){
console.log('setTimeout1');
})
setTimeout(function(){
console.log('setTimeout2');
})
console.log('world');
结果依次是:hello world setTimeout1 setTimeout2
任务队列

任务队列存在两种类型,一种是microtask queue,还有一种是macrotask queue。
macro-task(宏任务)包括:script(整体代码),setTimeout,setInterval, setImmediate(node.js),I/O,UI rendering。
micro-task(微任务)包括:process.nextTick(node.js),Promises,Object.observe, MutationObserver。
事件循环主线程进入调用栈开始,看到setTimeout处理完就会添加macrotask 队列中。看到Promises处理完就会添加到microtask队列中。等到主线程调用栈都执行完任务后,会优先按执行microtask queue,全部执行完。然后才会取macrotask queue排在队首的执行。
看下面这段代码:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
new Promise((resolve, reject) => {
console.log(3);
Promise.resolve().then(() => {
console.log(4);
})
resolve();
}).then(() => {
console.log(6);
})
console.log(7);
执行顺序依次是:1 3 7 4 6 2 。这说明mic-rotask中的回调将优先执行。
ps:这个网站可以实现代码的可视化 latentflip.com/loupe/,有兴趣的可以看看。
参考文章: