concurrent模式中时间分片和让出主线程方式概述(精简版)
浏览器知识
浏览器需要保持流畅,需要尽量做到每秒(1000ms)60帧,也就是16ms要渲染一次页面;这16ms要做一堆工作(js执行(如:react的reconcile调和),页面渲染等),因为js执行会阻塞主进程使他没办法进行页面渲染,导致可能这1000ms只做了一次渲染,用户则会觉得卡顿。所以要保证流畅度,需控制js的执行时间。
时间分片机制
react设计了时间分片机制,一个时间分片为5ms,时间分片内任务没执行完,他会被react挂起让出主线程,则这一帧还剩余11ms用来干别的事(渲染ui等)。
concurrent模式核心原理
react通过MessageChannel/requestIdleCallback/setTimeout(不同优先级不同浏览器使用不同的api)三者的机制是当前 时间分片(5ms) 结束后,若任务还没执行完,将任务暂时挂起到任务队列,并且记录当前fiber节点的位置(使用全局指针 workInProgressRoot、workInProgress 记录正在处理的 fiber tree 的 root node 和 fiber node。),让出主线程去执行用户输入/ui渲染/网络等高优先级操作,如果高优先级操作产生高优先级的任务,此时需要马上响应,将高优先级放到任务队列顶层,等到下一次事件循环时取出顶层任务(高优先级任务or挂起的任务)继续执行。按以上机制循环往复,直到执行完毕。(每个任务都有自己的过期时间,如果到了过期时间他也会变成一个类似高优先级任务,在下一个时间片(5ms)优先执行。
concurrent模式中时间分片和让出主线程方式概述(详细版)
在 React 中,时间分片和让出主线程的概念是为了实现 高效的任务调度,使得界面在复杂的更新操作中保持流畅,避免出现卡顿或延迟。React 使用 时间分片(time slicing)和 任务调度(task scheduling)来确保在执行任务时能根据任务的优先级分配时间片,及时让出主线程,从而可以做更多的关键操作,如 UI 渲染、用户交互处理等。
为什么要让出主线程?
浏览器的主线程在执行 JavaScript 任务时,也需要处理许多其他关键任务,比如:
- 渲染页面: 更新 DOM 和渲染页面内容。
- 用户交互: 处理用户输入、点击、滚动等操作。
- 网络请求: 处理异步请求的响应。
如果 JavaScript 长时间占用主线程不释放,浏览器就无法及时渲染页面或响应用户输入,导致页面卡顿或无响应。因此,React 会通过 时间分片 和 让出主线程 的方式,确保长时间运行的任务可以中断,并允许浏览器做一些更高优先级的操作,如渲染 UI 或处理用户交互。
React 中如何在两个时间片间让出主线程?
React 在更新组件或执行长时间运行的任务时,采用了时间分片和 优先级调度的策略。具体而言,React 会按照以下流程进行调度:
- 任务分割: 当需要进行复杂的更新(如重新渲染大量组件、计算大量状态等)时,React 会将任务分割为多个较小的子任务。每个子任务在一个时间片内执行,时间片通常为 5ms 左右。
- 任务调度: 每个任务的执行会检查当前时间片是否已用完。如果时间片已用完,React 会主动 让出主线程,将剩余的任务推入消息队列,并等待下一个时间片来继续执行。
- 让出主线程: 当 React 执行任务时,它会判断当前任务是否超出了预定的时间片。如果任务仍未完成,React 会使用
requestIdleCallback或MessageChannel(基于浏览器的消息传递机制)来请求下一个时间片继续执行当前任务,而在当前时间片结束时,浏览器可以用来处理更高优先级的任务,如 UI 渲染 或 用户交互处理。 - 让主线程做其他高优先级工作: 在每个时间片结束时,React 会根据优先级和状态的变化,让浏览器 处理渲染、响应用户输入等任务,以保证用户界面的流畅和响应性。
- 恢复任务: 当下一个时间片到来时,React 会继续从上次停止的地方恢复任务,直到任务完成。
具体的 React 调度机制:
React 使用的调度系统基于 任务队列和 优先级队列。在执行过程中,React 会根据任务的优先级来决定是否需要中断当前任务,让主线程做其他更重要的工作(如渲染和用户输入)。
React 会通过以下几种机制来控制时间分片和任务调度:
三者的机制是将当前时间分片(5ms)结束后,若任务还没执行完,将任务暂时挂起到任务队列(记录当前fiber节点的位置),让出主线程去执行用户输入/ui渲染/网络等高优先级操作,如果高优先级操作产生高优先级的任务,此时需要马上响应,将高优先级放到任务队列顶层,等到下一次事件循环时取出顶层任务(高优先级任务or挂起的任务)继续执行。按以上机制循环往复,直到执行完毕。(每个任务都有自己的过期时间,如果到了过期时间他也会变成一个类似高优先级任务,在下一个时间片(5ms)优先执行。
setTimeout/setInterval: 可以在 JavaScript 中通过setTimeout或setInterval来异步执行任务,虽然这种方式有时会有较高的延迟,但仍能保证任务不会完全阻塞主线程。MessageChannel: React 在实现时间分片时,常使用MessageChannel来比setTimeout更快地调度任务。MessageChannel允许更精确和低延迟的任务调度,并且它能够通过浏览器的主线程将任务放入队列并重新调度。requestIdleCallback: 当浏览器空闲时,requestIdleCallback允许开发者提交一些低优先级的任务,这些任务可以在浏览器空闲时执行,从而减少任务对主线程的占用。
生活中的比喻:
可以将浏览器的任务调度比作一个多线程的工厂生产线:
- 主线程:就像工厂的生产线,负责同时进行多项工作:生产商品(渲染 UI)、处理用户的需求(响应输入事件)、维护机器运转(执行 JavaScript 代码)。
- 时间片:就像生产线上的每一个时间段,工人有固定的时间来做每项工作,不能无限制地集中精力做一项任务。完成一个任务后,工人会让出生产线,休息片刻,给其他任务腾出时间。
- 任务中断与恢复:如果工人在一个任务上花费的时间过长,生产线就会暂停该任务,允许工人去处理其他更紧急的事情(如响应用户的需求或渲染新产品)。稍后,工人再回来继续做未完成的任务。
这种调度模型确保了 工厂(浏览器) 既能高效地执行任务,也不会因为某个单一任务占用过多时间而导致整个生产线卡顿。
总结:
在 React 中,让出主线程 是为了确保用户界面的 流畅性 和 响应性。React 通过 时间分片 和 优先级调度 将长时间运行的任务拆分为较小的子任务,并允许主线程在两个时间片之间中断任务,给浏览器足够的时间来执行高优先级的任务(如渲染界面和响应用户交互)。这帮助 React 在进行复杂的状态更新时,保持页面的流畅性并避免 UI 卡顿。