JavaScript如何在单线程上处理任务?
为了避免JavaScript操作DOM时产生复杂同步问题,JavaScript引擎运行在渲染进程的主线程上,即JavaScript是单线程执行的。但JavaScript在执行中,需要处理各种任务,例如对DOM,样式,布局等的处理。因此,就需要一个系统,来保证这些待处理的任务能够在主线程中有条不紊的执行。为了说明这一点,让我们由简至繁来看几个场景:
- 仅需处理预设任务:
- 在单线程中顺次处理预设任务,执行完毕后,线程退出;
- 还要接收处理新产生任务:
- 引入事件:线程等待接收事件,收到事件后线程被激活执行;
- 引入循环:线程循环执行;
- 还要处理其它线程发送到主线程的任务:
- 引入消息队列:将其它线程中产生的新任务入队,主线程取队头事件进行处理;
- 还要处理其它进程发来的任务:
- 引入IO线程:渲染进程使用IO线程,来专门负责接收其它进程发来的消息,由IO线程将消息组装成任务发送给主线程;
- 还要安全退出事件循环:
- 引入退出标志:执行完任务后,判断是否存在退出标志;
上述过程展示了JavaScript渲染主线程工作的大致关键节点。其中,执行任务所用到的核心数据结构就是消息队列。队列先进先出的特性保证了所有任务都可以被有条不紊的执行,但这也意味着,对任何一个任务,无论优先级高低或执行时间长短,都必须等待前面的任务执行完毕。
如何提高监听DOM变化的时间精度?
在前端开发中,监听DOM元素变化是一个常见的需求。而在实践中,DOM元素的变化往往非常频繁,如果我们对DOM元素的变化同步监听,频繁的变化会大量占用渲染主线程,使得渲染主线程的执行效率下降。而如果异步监听DOM元素的变化,由于任务需要在消息队列中排队等待,且消息队列中还可能穿插存在一些系统级任务,因此,无法保证DOM元素监听的实时性。为了平衡这两点,引入了宏任务与微任务的概念。
我们将消息队列中的任务视为宏任务。在上面的例子中,如果将监听DOM元素当作一个宏任务,这个宏任务的执行,依赖于DOM元素的变化。因此,我们将宏任务依赖的子任务,当作微任务。为了保证微任务的有序执行,采用了消息队列的思路,引入了微任务队列。当DOM变化时,就会先将变化添加到微任务队列中,从而避免对主线程占用。而当宏任务在主线程中被执行完毕后,主线程会先检测宏任务的微任务队列,微任务会依次出队,被主线程执行。
从上面的分析可以看出,微任务的执行时长会影响到当前宏任务的执行时长。因此,在日常编码中,要注意尽可能缩短微任务执行时间,避免对性能产生影响。
异步回调
异步回调主要有两种实现方式:
- 宏任务封装异步回调;利用消息队列实现。
- 微任务;在宏任务结束之前,利用微任务队列实现。
在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。这是因为,微任务中产生的宏任务,会插入到消息队列(或延迟队列)末尾,因此需要到下次事件循环才能执行。而微任务在本次事件循环中,就能够在微任务队列中被取出执行。