剖析 React 源码:组件更新流程(一)

这是个人剖析 React 源码的第五篇文章。这篇文章开始将会带着你们学习组件更新过程相关的内容,尽量的脱离源码来了解原理,下降你们的学习难度。前端

文章相关资料

组件更新流程中你能学到什么?

文章分为三部分,在这部分的文章中你能够学习到以下内容:node

  • setState 背后的批量更新如何实现
  • Fiber 是什么?有什么用?
  • 如何调度任务

在另外的两篇文章中你能够学习到如何调和组件及渲染组件的过程。react

setState 背后的批量更新如何实现

想必你们都知道大部分状况下屡次 setState 不会触发屡次渲染,而且 state 的值也不是实时的,这样的作法可以减小没必要要的性能消耗。git

handleClick () {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}
复制代码

那么这个行为是如何实现的呢?答案是批量更新。接下来咱们就来学习批量更新是如何实现的。github

其实这个背后的原理至关之简单。假如 handleClick 是经过点击事件触发的,那么 handleClick 其实差很少会被包装成这样:算法

isBatchingUpdates = true
try {
    handleClick()
} finally {
    isBatchingUpdates = false
    // 而后去更新
}
复制代码

在执行 handleClick 以前,其实 React 就会默认此次触发事件的过程当中若是有 setState 的话就应该批量更新。浏览器

当咱们在 handleClick 内部执行 setState 时,更新状态的这部分代码首先会被丢进一个队列中等待后续的使用。而后继续处理更新的逻辑,毕竟触发 setState 确定会触发一系列组件更新的流程。可是在这个流程中若是 React 发现须要批量更新 state 的话,就会当即中断更新流程。markdown

也就是说,虽然咱们在 handleClick 中调用了三次 setState,可是并不会走完三次的组件更新流程,只是把更新状态的逻辑丢到了一个队列中。当 handleClick 执行完毕以后会再执行一次组件更新的流程。数据结构

另外组件更新流程实际上是有两个大相径庭的分支的。一种就是触发更新之后一次完成所有的组件更新流程;另外一种是触发更新之后分时间片断完成全部的组件更新,用户体验更好,这种方式被称之为任务调度。若是你想详细了解这一块的内容,能够阅读我以前 写的文章ide

固然本文也会说起一部分调度相关的内容,毕竟这块也包含在组件更新流程中。可是在学习任务调度以前,咱们须要先来学习下 fiber 相关的内容,由于这块内容是 React 实现各类这些新功能的基石。

Fiber 是什么?有什么用?

在了解 Fiber 以前,咱们先来了解下为何 React 官方要费那么大劲去重构 React。

在 React 15 版本的时候,咱们若是有组件须要更新的话,那么就会递归向下遍历整个虚拟 DOM 树来判断须要更新的地方。这种递归的方式弊端在于没法中断,必须更新完全部组件才会中止。这样的弊端会形成若是咱们须要更新一些庞大的组件,那么在更新的过程当中可能就会长时间阻塞主线程,从而形成用户的交互、动画的更新等等都不能及时响应。

React 的组件更新过程简而言之就是在持续调用函数的一个过程,这样的一个过程会造成一个虚拟的调用栈。假如咱们控制这个调用栈的执行,把整个更新任务拆解开来,尽量地将更新任务放到浏览器空闲的时候去执行,那么就能解决以上的问题。

那么如今是时候介绍 Fiber 了。Fiber 从新实现了 React 的核心算法,带来了杀手锏增量更新功能。它有能力将整个更新任务拆分为一个个小的任务,而且能控制这些任务的执行。

这些功能主要是经过两个核心的技术来实现的:

  • 新的数据结构 fiber
  • 调度器

新的数据结构 fiber

在前文中咱们说到了须要拆分更新任务,那么如何把控这个拆分的颗粒度呢?答案是 fiber。

咱们能够把每一个 fiber 认为是一个工做单元,执行更新任务的整个流程(不包括渲染)就是在反复寻找工做单元并运行它们,这样的方式就实现了拆分任务的功能。

拆分红工做单元的目的就是为了让咱们能控制 stack frame(调用栈中的内容),能够随时随地去执行它们。由此使得咱们在每运行一个工做单元后均可以按状况继续执行或者中断工做(中断的决定权在于调度算法)。

那么 fiber 这个数据结构到底长什么样呢?如今就让咱们来一窥究竟。

fiber 内部其实存储了不少上下文信息,咱们能够把它认为是改进版的虚拟 DOM,它一样也对应了组件实例及 DOM 元素。同时 fiber 也会组成 fiber tree,可是它的结构再也不是一个树形,而是一个链表的结构。

如下是 fiber 中的一些重要属性:

{
  ...
  // 浏览器环境下指 DOM 节点
  stateNode: any,
    
  // 造成列表结构
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,

  // 更新相关
  pendingProps: any,  // 新的 props
  memoizedProps: any,  // 旧的 props
  // 存储 setState 中的第一个参数
  updateQueue: UpdateQueue<any> | null, 
  memoizedState: any, // 旧的 state
    
  // 调度相关
  expirationTime: ExpirationTime,  // 任务过时时间
    
  // 大部分状况下每一个 fiber 都有一个替身 fiber
  // 在更新过程当中,全部的操做都会在替身上完成,当渲染完成后,
  // 替身会代替自己
  alternate: Fiber | null,

  // 先简单认为是更新 DOM 相关的内容
  effectTag: SideEffectTag, // 指这个节点须要进行的 DOM 操做
  // 如下三个属性也会造成一个链表
  nextEffect: Fiber | null, // 下一个须要进行 DOM 操做的节点
  firstEffect: Fiber | null, // 第一个须要进行 DOM 操做的节点
  lastEffect: Fiber | null, // 最后一个须要进行 DOM 操做的节点,同时也可用于恢复任务
  ....
}
复制代码

总的来讲,咱们能够认为 fiber 就是一个工做单元的数据结构表现,固然它一样也是调用栈中的一个重要组成部分。

Fiber 和 fiber 不是同一个概念。前者表明新的调和器,后者表明 fiber node,也能够认为是改进后的虚拟 DOM。

调度器简介

每次有新的更新任务发生的时候,调度器都会按照策略给这些任务分配一个优先级。好比说动画的更新优先级会高点,离屏元素的更新优先级会低点。

经过这个优先级咱们能够获取一个该更新任务必须执行的截止时间,优先级越高那么截止时间就越近,反之亦然。这个截止时间是用来判断该任务是否已通过期,若是过时的话就会立刻执行该任务。

而后调度器经过实现 requestIdleCallback 函数来作到在浏览器空闲的时候去执行这些更新任务。

这其中的实现原理略微复杂。简单来讲,就是经过定时器的方式,来获取每一帧的结束时间。获得每一帧的结束时间之后咱们就能判断当下距离结束时间的一个差值。

若是还未到结束时间,那么也就意味着我能够继续执行更新任务;若是已通过告终束时间,那么就意味着当前帧已经没有时间给我执行任务了,必须把执行权交还给浏览器,也就是打断任务的执行。

另外当开始执行更新任务(也就是寻找工做单元并执行的过程)时,若是有新的更新任务进来,那么调度器就会按照二者的优先级大小来进行决策。若是新的任务优先级小,那么固然继续当下的任务;若是新的任务优先级大,那么会打断任务并开始新的任务。

以上就是调度器的原理简介,若是你想了解更多的内容,能够阅读我以前写的文章: 剖析 React 源码:调度原理

小结

如今是时候把文章中说起到的内容整合起来了,另外咱们假设更新任务一定会触发调度。

当交互事件调用 setState 后,会触发批量更新,在整个交互事件回调执行完以前 state 都不会发生变动。

回调执行完毕后,开始更新任务,并触发调度。调度器会给这些更新任务一一设置优先级,而且在浏览器空闲的时候去执行他们,固然任务过时除外(会马上触发更新,再也不等待)。

若是在执行更新任务的时候,有新的任务进来,会判断两个任务的优先级高低。假如新任务优先级高,那么打断旧的任务,从新开始,不然继续执行任务。

最后

阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。

另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。

最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。

相关文章
相关标签/搜索