为什么要学习事件循环
只有当我们了解事件循环的运行机制,并将其刻入脑海,我们对整个JavaScript的运行机制才有进一步的把控,它会在潜意识中深切的影响我们编程时的思考代码的方式。
单线程的JavaScript语言,意味着同一时间只能处理一个任务,执行一行代码。为了整个程序的顺利进行,事件循环机制协调了事件、用户网页交互、脚本、ui渲染和异步的网络请求,目的是不阻塞主线程。
JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。 任务包括 script(整体代码)、 setTimeout、setInterval、DOM渲染、DOM事件、Promise、XMLHTTPREQUEST等
1、主线程中的任务执行完后,才执行任务队列中的任务
2、有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
3、比如多个 setTimeout 同时到时间了,就要依次执行
执行栈
也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
主线程
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。 当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
过程说明:
1、执行第1行的宏任务script,输出
3、执行到Promise,遇到第一个then()。这是个微任务,将其压入微任务队列
4、再执行第15行的主线程代码,输出
6、在同一轮事件循环中,由于微任务的优先级比宏任务高,所以即使刚刚的setTimeout定时器先入宏任务队列,promise.then()后入微任务队列,微任务代码仍然先推到执行栈中,然后执行结束,输出
7、输出promise1之后,继续执行第二个promise.then(),同步骤2,将任务放入微任务队列,此时,主线程为空了。又要开始检查微任务队列是否还有任务等待,读取到promise2的微任务,将其推入执行栈,输出:
定时器
setTimeout延迟时间结束之后添加到一个新的Task中
宏任务(Task)
在事件循环中,每一个Task执行结束之后,在下一个Task开始之前,浏览器可以对页面进行重新渲染。其中setTimeout延时不一定精准,因为setTimeout延迟时间结束之后添加到一个新的Task中,而不是立即执行,需要等待上一次的Task全部执行完成后才能执行,而执行完成的时间谁都无法保证。
微任务(Microtask)
需要在当前的Task执行结束之后,立即执行完所有的microtask,而不需要重新分配一个新的宏任务,这样可以减小一点性能的开销。通过对上面的输出的分析,大家应该知道:microtask 任务队列是一个与 task 任务队列相互独立的队列,microtask 任务将会在每一个 task 任务执行结束之后执行。每一个 task 中产生的 microtask 都将会依次添加到 microtask 队列中,microtask 中产生的 microtask 将会添加至当前队列的尾部,并且 microtask 会按序的处理完队列中的所有任务。microtask 类型的任务目前包括了 MutationObserver 以及 Promise 的回调函数。
思考题
如下代码:此时切换页面(选项卡)的 浏览页面UI 是否仍然响应?
如下代码:是否存在堆栈溢出错误?
上面两个例子很好的解释了事件循环中宏任务和微任务的的差异。
总结
JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。其次微任务优先于宏任务执行。所以如果有希望优先执行的逻辑,放入微任务队列会宏任务队列更早的被执行。在事件循环中,每一个Task执行结束之后,在下一个Task开始之前,浏览器可以对页面进行重新渲染。
第一次码字,如有不正确的地方,希望路过的老师傅不吝赐教,如果看完觉得没有浪费时间麻烦点个赞吧!
参考资料: [译] 深入理解 JavaScript 事件循环