当前位置: 首页 > 科技观察

为什么同样的逻辑在不同的前端框架下效果不一样

时间:2023-03-22 01:26:01 科技观察

大家好,我是Kason。前端框架往往有“将多个自变量变化触发的更新合并为一次执行”的批处理场景。不同类型的框架,批处理的时机是不同的。例如,下面的Svelte代码在点击H1后执行onClick回调函数,触发了三个更新。由于批处理,三个更新合并为一个。然后以同步、microtask、macrotask的形式打印渲染结果:{count}同样的逻辑使用了不同的框架实现,打印结果如下:Vue3:同步结果:0微任务结果:3宏任务结果:3Svelte:同步结果:0微任务结果:3宏任务结果:3LegacyReact:同步结果:0微任务结果:3宏任务结果:3ConcurrentReact:同步结果:0微任务结果:0宏任务结果:34实现的Demo地址:React[1]Vue3[2]Svelte[3]本质原因是一些框架使用宏任务实现批处理,有些框架使用微任务来实现批处理。本文将解释宏任务和微任务的由来,以及它们与批处理的关系。如何调度任务先放完整的流程图,以便有一个整体的印象:事件循环流程图默认情况下,浏览器中的每个标签页(以Chrome为例)对应一个渲染进程,渲染进程包括主线程和合成线程,IO线程和其他线程。主线程很忙,处理DOM,计算样式,处理布局,处理事件响应,执行JS等等。这里有两个问题需要解决:这些任务不仅来自线程内部,也来自外部。如何安排这些任务?主线程工作时,新任务如何参与调度?第一个问题的答案是:“消息队列”所有参与调度的任务都会加入到任务队列中。根据队列“先进先出”的特点,最早进入队列的任务将被最先处理。伪代码描述如下://从任务队列中取出任务consttask=taskQueue.takeTask();//执行任务processTask(task);其他进程通过IPC将任务发送到渲染进程的IO线程,IO线程将任务发送到主线程的任务队列,例如:鼠标点击后,浏览器进程发送"click事件"通过IPC传递给IO线程,IO线程将其发送到任务队列。事件”发送到IO线程,IO线程发送到任务队列如何调度新任务第二个问题的答案是:“事件循环”主线程会在循环语句中执行任务。作为循环继续,新加入的任务会被插入到队尾,旧的任务会被取出执行,伪代码描述如下://退出事件循环标识letkeepRunning=true;//主线程函数MainThread(){//循环执行任务while(true){//从任务队列中取出任务consttask=taskQueue.takeTask();//执行任务processTask(task);if(!keepRunning){break;}}}延时任务入除了任务队列,浏览器还按照WHATWG标准实现了一个延迟队列,用于存放需要延迟的任务(比如setTimeout),伪代码如下:functionMainThread(){while(true){consttask=taskQueue.takeTask();processTask(task);//执行延迟队列中的任务processDelayTask()if(!keepRunning){break;}}}当前循环任务执行时(即processTask执行完后)会执行processDelayTask,检查是否有延时任务超时,有任务超时则执行。processDelayTask的执行时机在processTask之后,所以当任务执行时间比较长的时候,延迟的任务可能不会按计划执行。考虑以下代码:functionsayHello(){console.log('hello')}functiontest(){setTimeout(sayHello,0);for(leti=0;i<5000;i++){console.log(i);}}即使test()将延迟任务sayHello的延迟时间设置为0,也需要等待test所在的任务执行完才能执行,所以sayHello最终的延迟时间大于设置的时间。宏任务和微任务添加到任务队列的新任务需要等待队列中的其他任务执行完毕后才能执行,这对“紧急情况下需要先执行的任务”不利。为了解决时效性问题,将任务队列中的任务称为宏任务。在宏任务执行过程中,可以生成微任务,并存储在任务执行上下文中的微任务队列中。即流程图右侧部分:事件循环流程图会在macrotask执行结束前遍历其microtask队列,批量执行macrotask执行过程中产生的microtasks。MutationObserver微任务是如何在兼顾性能的同时解决时效性问题的?考虑用于监视DOM更改的微任务API-MutationObserver。当同一个macrotask发生多次DOM变化时,会产生多个MutationObservermicrotasks,执行时机在macrotask执行结束之前。相比作为一个新的macrotask进入队列等待执行,时效性得到了保证。同时,由于微任务队列中的微任务是分批执行的,相比每次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