大家好,为啥我卡颂。同样同前 前端框架中经常有「将多个自变量变化触发的逻端框更新合并为一次执行」的批处理场景,框架的辑不架中类型不同,批处理的效果时机也不同。 比如如下Svelte代码,不同点击H1后执行onClick回调函数,为啥触发三次更新。同样同前由于批处理,逻端框三次更新会合并为一次。辑不架中 接着分别以同步、效果微任务、不同宏任务的为啥形式打印渲染结果: 同样的逻辑用不同框架实现,打印结果如下: 4种实现的同样同前Demo地址:React[1]Vue3[2]Svelte[3] 本质原因在于:有的框架使用宏任务实现批处理,有的逻端框框架使用微任务实现批处理。 本文接下来会讲解宏任务、微任务的起源,以及他们与批处理的关系。 先放上完整流程图,方便有个整体印象: 事件循环流程图 默认情况下,浏览器(以Chrome为例)中每个Tab页对应一个渲染进程,渲染进程包含主线程、合成线程、云服务器提供商IO线程等多个线程。 主线程的工作非常繁忙,要处理DOM、计算样式、处理布局、处理事件响应、执行JS等。 这里有两个问题需要解决: 第一个问题的答案是:「消息队列」 所有参与调度的任务会加入任务队列中。根据队列「先进先出」的特性,最早入队的任务会被最先处理。用伪代码描述如下: 其他进程通过IPC将任务发送给渲染进程的IO线程,IO线程再将任务发送给主线程的任务队列,比如: 第二个问题的答案是:「事件循环」 主线程会在循环语句中执行任务。随着循环一直进行下去,新加入的任务会插入队列末尾,亿华云老任务会被取出执行。用伪代码描述如下: 除了任务队列,浏览器还根据WHATWG标准,实现了延迟队列,用于存放需要被延迟执行的任务(如setTimeout),伪代码如下: 当本轮循环任务执行完后(即执行完processTask后),会执行processDelayTask检查是否有延迟任务到期,如果有任务过期则执行他。 介于processDelayTask的执行时机在processTask之后,所以当任务的执行时间比较长,可能会导致延迟任务无法按期执行。考虑如下代码: 即使将延迟任务sayHello的延迟时间设为0,也需要等待test所在任务执行完后才能执行,所以sayHello最终的延迟时间是大于设定时间的。 加入任务队列的新任务需要等待队列中其他任务都执行完后才能执行,这对于「突发情况下需要优先执行的任务」是不利的。 为了解决时效性问题,任务队列中的任务被称为宏任务,服务器租用在宏任务执行过程中可以产生微任务,保存在该任务执行上下文中的微任务队列中。 即流程图中右边的部分: 事件循环流程图 在宏任务执行结束前会遍历其微任务队列,将该宏任务执行过程中产生的微任务批量执行。 微任务是如何解决时效性问题同时又兼顾性能呢? 考虑用于监控DOM变化的微任务API —— MutationObserver。 当同一个宏任务中发生多次DOM变化,会产生多个MutationObserver微任务,其执行时机是该宏任务执行结束前,相比于作为新的宏任务进入队列等待执行,保证了时效性。 同时,由于微任务队列内的微任务被批量执行,相比于每次DOM变化都同步执行回调,性能更佳。 框架中批处理的实现本质和MutationObserver非常类似。利用了宏任务、微任务异步执行的特性,将更新打包后执行。 只不过不同框架由于更新粒度不同,比如Vue3、Svelte更新粒度很细,所以使用微任务实现批处理。 React更新粒度很粗,但内部实现复杂,即有宏任务场景也有微任务的场景。 [1]React: https://codesandbox.io/s/react-concurrent-mode-demo-forked-t8mil?file=/src/index.js[2]Vue3: https://codesandbox.io/s/crazy-rosalind-wqj0c?file=/src/App.vue[3]Svelte: https://svelte.dev/repl/1e4e4e44b9ca4e0ebba98ef314cfda54?version=3.44.1如何调度任务
如何调度新任务
延迟任务
宏任务与微任务
MutationObserver
总结
参考资料