一言以蔽之--JavaScript事件循环

849 阅读7分钟

为什么要学习事件循环

只有当我们了解事件循环的运行机制,并将其刻入脑海,我们对整个JavaScript的运行机制才有进一步的把控,它会在潜意识中深切的影响我们编程时的思考代码的方式。

单线程的JavaScript语言,意味着同一时间只能处理一个任务,执行一行代码。为了整个程序的顺利进行,事件循环机制协调了事件、用户网页交互、脚本、ui渲染和异步的网络请求,目的是不阻塞主线程。

JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。 任务包括 script(整体代码)、 setTimeout、setInterval、DOM渲染、DOM事件、Promise、XMLHTTPREQUEST等

1、主线程中的任务执行完后,才执行任务队列中的任务

2、有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务

3、比如多个 setTimeout 同时到时间了,就要依次执行

执行栈

也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。 当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

过程说明:

1、执行第1行的宏任务script,输出

2、执行到setTimeout,不是马上就将其中的函数压入任务队列,而是将setTimeout放入系统中的定时器模块,开始计时,当定时器的时间到了,再将setTimeout内部的函数压入宏任务队列。

3、执行到Promise,遇到第一个then()。这是个微任务,将其压入微任务队列

4、再执行第15行的主线程代码,输出

5、此时,主线程是空:这就是主线程能够通过事件循环腾出手来执行已经存在于任务队列中(包括微任务队列+宏任务队列)的任务的标志。

6、在同一轮事件循环中,由于微任务的优先级比宏任务高,所以即使刚刚的setTimeout定时器先入宏任务队列,promise.then()后入微任务队列,微任务代码仍然先推到执行栈中,然后执行结束,输出

7、输出promise1之后,继续执行第二个promise.then(),同步骤2,将任务放入微任务队列,此时,主线程为空了。又要开始检查微任务队列是否还有任务等待,读取到promise2的微任务,将其推入执行栈,输出:

8、此时,主执行栈又为空了,再次检查微任务队列发现已无任务,检查宏任务队列,然后读取了setTimeout任务,并将其压入执行栈,然后输出:

9、现在,所有任务都执行结束了,宏任务微任务都没有了,开始休眠。

定时器

setTimeout延迟时间结束之后添加到一个新的Task中

定时器第一个参数是要延时执行的函数,可以是具名函数也可以是匿名函数;第二个参数,是这个函数需要延迟的时间参数delayTime,可以忽略不写,delayTime >= 0,即使delayTime为0,实际浏览器执行起来,最少的时间也是延迟4ms执行,一般不建议这么写。我们知道浏览器一般的刷新频率是60HZ,也就是1000ms刷新60次,所以有些异步操作DOM最低是1000/60 ≈ 16,也就是大约16ms更新一次DOM。

宏任务(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中有宏任务和微任务。setTimeout回调是宏任务,而Promise回调是微任务。主要的区别在于他们的执行方式。宏任务在单个循环周期中一次一个地推入堆栈,但是微任务队列总是在执行后返回到事件循环之前清空。因此,如果你以计算机运行能力的速度向这个队列添加,那么你就永远在处理微任务。每次调用'foo'都会继续在微任务队列上添加另一个'foo'回调,因此事件循环无法继续处理其他事件(滚动,单击等),直到该队列完全清空为止。 因此,点击页面不会有其他的交互,它会阻止渲染。

如下代码:是否存在堆栈溢出错误?

解析:浏览器的主要组件包括调用堆栈,事件循环,任务队列和Web API。 像setTimeout,setInterval和Promise这样的全局函数不是JavaScript的一部分,而是 Web API 的一部分。 JS调用栈是后进先出的。引擎每次从堆栈中取出一个函数,然后从上到下依次运行代码。每当它遇到一些异步代码,如setTimeout,它就把它交给Web API。因此,每当事件被触发时,callback 都会被发送到任务队列。事件循环不断地监视任务队列,并按它们排队的顺序一次处理一个回调。每当调用堆栈为空时,Event loop获取回调并将其放入堆栈中进行处理。请记住,如果调用堆栈不是空的, 则事件循环不会将任何回调推入堆栈。

上面两个例子很好的解释了事件循环中宏任务和微任务的的差异。

总结

JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。其次微任务优先于宏任务执行。所以如果有希望优先执行的逻辑,放入微任务队列会宏任务队列更早的被执行。在事件循环中,每一个Task执行结束之后,在下一个Task开始之前,浏览器可以对页面进行重新渲染。

第一次码字,如有不正确的地方,希望路过的老师傅不吝赐教,如果看完觉得没有浪费时间麻烦点个赞吧!

参考资料: [译] 深入理解 JavaScript 事件循环