您现在的位置是:亿华云 > 应用开发
为啥同样的逻辑在不同前端框架中效果不同
亿华云2025-10-09 01:16:45【应用开发】6人已围观
简介大家好,我卡颂。前端框架中经常有「将多个自变量变化触发的更新合并为一次执行」的批处理场景,框架的类型不同,批处理的时机也不同。比如如下Svelte代码,点击H1后执行onClick回调函数,触发三次更
大家好,为啥我卡颂。同样同前
前端框架中经常有「将多个自变量变化触发的逻端框更新合并为一次执行」的批处理场景,框架的辑不架中类型不同,批处理的效果时机也不同。
比如如下Svelte代码,不同点击H1后执行onClick回调函数,为啥触发三次更新。同样同前由于批处理,逻端框三次更新会合并为一次。辑不架中
接着分别以同步、效果微任务、不同宏任务的为啥形式打印渲染结果:
<script> let count = 0; let dom; const onClick = () => { // 三次更新合并为一次 count++; count++; count++; console.log("同步结果:", dom.innerText); Promise.resolve().then(() => { console.log("微任务结果:", dom.innerText); }); setTimeout(() => { console.log("宏任务结果:", dom.innerText); }); } </script> <h1 bind:this={ dom} on:click={ onClick}>{ count}</h1>同样的逻辑用不同框架实现,打印结果如下:
Vue3:同步结果:0 微任务结果:3 宏任务结果:3 Svelte:同步结果:0 微任务结果:3 宏任务结果:3 Legacy React:同步结果:0 微任务结果:3 宏任务结果:3 Concurrent React:同步结果:0 微任务结果:0 宏任务结果:34种实现的同样同前Demo地址:React[1]Vue3[2]Svelte[3]
本质原因在于:有的框架使用宏任务实现批处理,有的逻端框框架使用微任务实现批处理。
本文接下来会讲解宏任务、微任务的起源,以及他们与批处理的关系。
如何调度任务
先放上完整流程图,方便有个整体印象:
事件循环流程图
默认情况下,浏览器(以Chrome为例)中每个Tab页对应一个渲染进程,渲染进程包含主线程、合成线程、亿华云IO线程等多个线程。
主线程的工作非常繁忙,要处理DOM、计算样式、处理布局、处理事件响应、执行JS等。
这里有两个问题需要解决:
这些任务不仅来自线程内部,也可能来自外部,如何调度这些任务? 主线程在工作过程中,新任务如何参与调度?第一个问题的答案是:「消息队列」
所有参与调度的任务会加入任务队列中。根据队列「先进先出」的特性,最早入队的任务会被最先处理。用伪代码描述如下:
// 从任务队列中取出任务 const task = taskQueue.takeTask(); // 执行任务 processTask(task);其他进程通过IPC将任务发送给渲染进程的IO线程,IO线程再将任务发送给主线程的任务队列,比如:
鼠标点击后,浏览器进程通过IPC将“点击事件”发送给IO线程,IO线程将其发送给任务队列 资源加载完成后,网络进程通过IPC将“加载完成事件”发送给IO线程,IO线程将其发送给任务队列如何调度新任务
第二个问题的答案是:「事件循环」
主线程会在循环语句中执行任务。随着循环一直进行下去,新加入的任务会插入队列末尾,服务器租用老任务会被取出执行。用伪代码描述如下:
// 退出事件循环的标识 let keepRunning = true; // 主线程 function MainThread() { // 循环执行任务 while(true) { // 从任务队列中取出任务 const task = taskQueue.takeTask(); // 执行任务 processTask(task); if (!keepRunning) { break; } } }延迟任务
除了任务队列,浏览器还根据WHATWG标准,实现了延迟队列,用于存放需要被延迟执行的任务(如setTimeout),伪代码如下:
function MainThread() { while(true) { const task = taskQueue.takeTask(); processTask(task); //执行延迟队列中的任务 processDelayTask() if (!keepRunning) { break; } } }当本轮循环任务执行完后(即执行完processTask后),会执行processDelayTask检查是否有延迟任务到期,如果有任务过期则执行他。
介于processDelayTask的执行时机在processTask之后,所以当任务的执行时间比较长,可能会导致延迟任务无法按期执行。考虑如下代码:
function sayHello() { console.log(hello) } function test() { setTimeout(sayHello, 0); for (let i = 0; i < 5000; i++) { console.log(i); } } test()即使将延迟任务sayHello的延迟时间设为0,也需要等待test所在任务执行完后才能执行,所以sayHello最终的延迟时间是大于设定时间的。
宏任务与微任务
加入任务队列的新任务需要等待队列中其他任务都执行完后才能执行,这对于「突发情况下需要优先执行的任务」是不利的。
为了解决时效性问题,任务队列中的任务被称为宏任务,服务器托管在宏任务执行过程中可以产生微任务,保存在该任务执行上下文中的微任务队列中。
即流程图中右边的部分:
事件循环流程图
在宏任务执行结束前会遍历其微任务队列,将该宏任务执行过程中产生的微任务批量执行。
MutationObserver
微任务是如何解决时效性问题同时又兼顾性能呢?
考虑用于监控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
很赞哦!(58246)
相关文章
- 国内域名
- Oracle 12c R2中的ADG会话保留特性
- Oracle RAC环境下的应用连续性
- MySQL 中with rollup总结
- 当投资者经过第二阶段的认真学习之后又充满了信心,认为自己可以在市场上叱咤风云地大干一场了。但没想到“看花容易绣花难”,由于对理论知识不会灵活运用.从而失去灵活应变的本能,就经常会出现小赢大亏的局面,结果往往仍以失败告终。这使投资者很是困惑和痛苦,不知该如何办,甚至开始怀疑这个市场是不是不适合自己。在这种情况下,有的人选择了放弃,但有的意志坚定者则决定做最后的尝试。
- 7大绝招帮你轻轻松松提升MySQL性能
- Database Sharding 架构深度解析指南
- 如何更好地理解递归算法?Python实例详解
- 在更换域名后,并不是就万事大吉了,我们需要将旧域名做301重定向到新域名上,转移旧域名的权重到新域名上。
- 如何理解并正确使用MySQL索引