事件循环就是Event Loop,是JavaScript 一个特殊的地方。特殊就在于JavaScript 是单线程语言,注定了对异步操作的处理有别于多线程语言。执行栈和任务队列是JavaScript 执行异步任务的管理方式,宏任务和微任务是js异步任务更细粒度的划分。
单线程
进程和线程是操作系统中的概念,线程是进程的最小单位,一个进程可以包含多个线程,此处不再赘述。JavaScript 在设计之初便是单线程,程序运行时,只有一个线程存在,在特定的时候只能有特定的代码被执行。这和 JavaScript 的用途有关,它是一门浏览器脚本语言,通常是用来操作 DOM 的,如果是多线程,一个线程进行了删除 DOM 操作,另一个添加 DOM,此时该如何处理?所以 JavaScript 在设计之初便是单线程的。
虽然 HTML5 增加了 Web Work 可用来另开一个线程,但是该线程仍受主线程的控制,没有window对象、不能操作DOM,所以 JavaScript 的本质依然是单线程。所有同步、异步任务都是在主线程中执行。
单线程的 JavaScript 一段一段地执行,前面的执行完了,再执行后面的,试想一下,如果前一个任务需要执行很久,比如接口请求、I/O 操作,那后面的任务只能干等吗?干等不仅浪费了资源,而且页面的交互程度也很差。JavaScript 意识到了这个问题,他们将任务分成了同步任务和异步任务,对于二者有不同的处理。
明确什么是同步、异步任务
先看下面这段代码
let name = '小明' //同步任务1
window.addEventListener('popstate', (e) => { //同步任务2
console.log(e) //异步任务
})
这里是两条语句,一是声明name并赋初值,二是添加window的popstate事件的监听。对应的这里就产生了2个任务,是同步任务
,主线程按照代码书写顺序先执行任务1(name声明和赋值),再执行同步任务2(添加window的popstate事件的监听)。监听器回调里要去执行console.log(e)
,所以这条语句也产生了一个任务,而它是一个异步任务
,确切的说整个回调函数产生了一个异步任务。
执行栈、任务队列
首先,执行栈管理同步任务、任务队列管理异步任务。同步任务没啥,异步任务分为异步宏任务和异步微任务
,
常见的宏任务有setTimeout、setInterval;
常见的微任务有 Promise、nextTick(node.js 环境)。
下面就根据代码来分析事件执行顺序:
console.log(1) //同步任务A
setTimeout( //同步任务B
() => { console.log(2) } //任务B产生的异步宏任务
, 300)
new Promise( //同步任务C
(resolve) => { console.log(3); resolve(4); }
)
.then( //任务C执行过程中resolve(4)语句产生的异步微任务
(num) => { console.log(num) }
)
setTimeout( //同步任务D
()=> { console.log(5) } //任务D产生的异步宏任务
, 800)
下面用一张张图片画出各个阶段发生的事情。
图一:JavaScript 在执行时,同步任务会排好队,在主线程上按照顺序执行A!
完了B,B完了C,直到执行栈里再无任务),排队的地方叫执行栈(execution context stack)。执行A没啥问题,执行到B时,B是一个setTimeout任务,我们知道它会产生一个异步任务,但是没有立刻产生,而是等待300ms再产生个异步宏任务,这个异步任务的目的是去打印数字2。这个时候主线程没有傻傻的等300ms再去打印数字2,而是将任务挂起,继续往下走去执行同步任务C。
图二:主线程继续往下执行了同步任务C,这个时候同步任务C有两部分,一是console.log(3),二是resolve(4) ,所以先后执行了
console.log(3)
、resolve(4)
,我们知道resolve(4)执行完会返回promise对象,而promise对象里有自己的执行代码(这段代码里就是console.log(num) ),那么主线程是继续往下执行下一个同步任务?还是转到执行resolve(4)返回的promise对象中的代码呢?答案很明显,会执行下一个同步任务。这个时候resolve(4)所返回的promise对象就产生了一个异步微任务,主线程将它挂起,挂起的方法就是将它塞到异步微任务执行队列中,先不管他。图三:又是一个setTimeout,和上一个一样,只不过这个要等800ms才会产生一个异步宏任务。回头看看之前B产生的那个异步宏任务BB去哪儿了呢?别急,300ms还没到呢,主线程从A一路往下到D这个期间异步任务BB还没创建好呢,创建好后主线程会再把它挂起来。这个时候你可能有疑惑,我去,如果B那里设置延时不是300ms,而是3ms或者0呢?岂不是执行顺序会根据电脑性能乱掉?这里如果将 setTimeout 的第二个参数设置为 0,它表示主线程空闲之后尽早执行它的回调,HTML5 规定 setTimeout 的第二个参数不得小于 4 毫秒。
图四:300ms时间到,异步宏任务BB被创建成功,但也没有立即执行,而是被主线程塞到了宏任务执行队列中。
图五:执行栈空了,就把微任务执行队列中任务全部执行完。
图六:从宏任务执行队列中取出任务放到执行栈继续执行,此时异步宏任务DD执行也创建好了,被塞到了宏任务执行队列,等待下次被取出放到执行栈执行。
图七:执行完一个宏任务后,再去执行所有的微任务,微任务执行完后再取出
最后一个宏任务DD放到执行栈执行。后面每执行完一个宏任务后都去“照顾”一下微任务。直到所有的宏任务、微任务都执行完。
所以上面代码的打印顺序是1->3->4->2->5。
总结
总结JavaScript处理同步异步任务的方式是:用栈和队列调度分配任务,代码首次运行主线程按照同步任务顺序,依次执行,执行期间产生的异步宏任务则挂起不执行,挂起的方式是添加至宏任务执行队列,产生的微任务也挂起,方式是添加至微任务执行队列。待所有同步任务执行完成,主线程从宏任务执行队列取出最先添加进宏任务执行队列的任务,放入执行栈执行,执行完后立即去执行所有的微任务,微任务清空后再执行下一个宏任务,每执行完一个宏任务就去清空一下微任务,直到两个执行队列全清空。如此循环就是Event Loop事件循环。