乐趣区

关于前端:为啥同样的逻辑在不同前端框架中效果不同

大家好,我卡颂。

前端框架中常常有 将多个自变量变动触发的更新合并为一次执行 的批处理场景,框架的类型不同,批处理的机会也不同。

比方如下 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 宏工作后果:3

4 种实现的 Demo 地址:React
Vue3
Svelte

实质起因在于:有的框架应用 宏工作 实现批处理,有的框架应用 微工作 实现批处理。

本文接下来会解说 宏工作 微工作 的起源,以及他们与批处理的关系。

欢送退出人类高质量前端框架群,带飞

如何调度工作

先放上残缺流程图,不便有个整体印象:

默认状况下,浏览器(以 Chrome 为例)中每个 Tab 页对应一个渲染过程,渲染过程蕴含主线程、合成线程、IO线程等多个线程。

主线程的工作十分忙碌,要解决 DOM、计算款式、解决布局、处理事件响应、执行JS 等。

这里有两个问题须要解决:

  1. 这些工作不仅来自线程外部,也可能来自内部,如何调度这些工作?
  2. 主线程在工作过程中,新工作如何参加调度?

第一个问题的答案是:音讯队列

所有参加调度的工作会退出工作队列中。依据队列 先进先出 的个性,最早入队的工作会被最先解决。用伪代码形容如下:

// 从工作队列中取出工作
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 十分相似。利用了 宏工作 微工作 异步执行的个性,将更新打包后执行。

只不过不同框架因为更新粒度不同,比方 Vue3Svelte 更新粒度很细,所以应用 微工作 实现批处理。

React更新粒度比拟粗,但外部实现比较复杂,即有宏工作的场景也有微工作的场景。

退出移动版