剖析 React 源码:render 流程(二)

这是个人剖析 React 源码的第三篇文章,若是你没有阅读过以前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。前端

文章相关资料

此篇文章内容衔接 render 流程(一),固然不看上一篇文章也没什么问题,由于内容并无强相关。react

如今请你们打开 个人代码 并定位到 react-dom 文件夹下的 src 中的 ReactDOM.js 文件,今天的内容会从这里开始。git

ReactRoot.prototype.render

在上一篇文章中,咱们介绍了当 ReactDom.render 执行时,内部会首先判断是否已经存在 root,没有的话会去建立一个 root。在今天的文章中,咱们将会了解到存在 root 之后会发生什么事情。github

你们能够先定位到代码的第 592 行。性能优化

你们能够看到,在上述的代码中调用了 unbatchedUpdates 函数,这个函数涉及到的知识其实在 React 中至关重要。dom

你们都知道多个 setState 一块儿执行,并不会触发 React 的屡次渲染。异步

// 虽然 age 会变成 3,但不会触发 3 次渲染
this.setState({ age: 1 })
this.setState({ age: 2 })
this.setState({ age: 3 })
复制代码

这是由于内部会将这个三次 setState 优化为一次更新,术语是批量更新(batchedUpdate),咱们在后续的内容中也能看到内部是如何处理批量更新的。函数

对于 root 来讲其实不必去批量更新,因此这里调用了 unbatchedUpdates 函数来告知内部不须要批量更新。性能

而后在 unbatchedUpdates 回调内部判断是否存在 parentComponent。这一步咱们能够假定不会存在 parentComponent,由于不多有人会在 root 外部加上 context 组件。不存在 parentComponent 的话就会执行 root.render(children, callback),这里的 render 指的是 ReactRoot.prototype.render学习

render 函数内部咱们首先取出 root,这里的 root 指的是 FiberRoot,若是你想了解 FiberRoot 相关的内容能够阅读 上一篇文章。而后建立了 ReactWork 的实例,这块内容咱们没有必要深究,功能就是为了在组件渲染或更新后把全部传入 ReactDom.render 中的回调函数所有执行一遍。

接下来咱们来看 updateContainer 内部是怎么样的。

咱们先从 FiberRoot 的 current 属性中取出它的 fiber 对象,而后计算了两个时间。这两个时间在 React 中至关重要,所以咱们须要单独用一小节去学习它们。

时间

首先是 currentTime,在 requestCurrentTime 函数内部计算时间的最核心函数是 recomputeCurrentRendererTime

function recomputeCurrentRendererTime() {
  const currentTimeMs = now() - originalStartTimeMs;
  currentRendererTime = msToExpirationTime(currentTimeMs);
}
复制代码

now() 就是 performance.now(),若是你不了解这个 API 的话能够阅读下 相关文档originalStartTimeMs 是 React 应用初始化时就会生成的一个变量,值也是 performance.now(),而且这个值不会在后期再被改变。那么这两个值相减之后,获得的结果也就是如今离 React 应用初始化时通过了多少时间。

而后咱们须要把计算出来的值再经过一个公式算一遍,这里的 | 0 做用是取整数,也就是说 11 / 10 | 0 = 1

接下来咱们来假定一些变量值,代入公式来算的话会更方便你们理解。

假如 originalStartTimeMs2500,当前时间为 5000,那么算出来的差值就是 2500,也就是说当前距离 React 应用初始化已通过去了 2500 毫秒,最后经过公式得出的结果为:

currentTime = 1073741822 - ((2500 / 10) | 0) = 1073741572
复制代码

接下来是计算 expirationTime这个时间和优先级有关,值越大,优先级越高。而且同步是优先级最高的,它的值为 1073741823,也就是以前咱们看到的常量 MAGIC_NUMBER_OFFSET 加一。

computeExpirationForFiber 函数中存在不少分支,可是计算的核心就只有三行代码,分别是:

// 同步
expirationTime = Sync
// 交互事件,优先级较高
expirationTime = computeInteractiveExpiration(currentTime)
// 异步,优先级较低
expirationTime = computeAsyncExpiration(currentTime)
复制代码

接下来咱们就来分析 computeInteractiveExpiration 函数内部是如何计算时间的,固然 computeAsyncExpiration 计算时间的方式也是相同的,无非更换了两个变量。

以上这些代码其实就是公式,咱们把具体的值代入就能算出结果了。

time = 1073741822 - ((((1073741822 - 1073741572 + 15) / 10) | 0) + 1) * 10 = 1073741552
复制代码

另外在 ceiling 函数中的 1 * bucketSizeMs / UNIT_SIZE 是为了抹平一段时间内的时间差,在抹平的时间差内无论有多少个任务须要执行,他们的过时时间都是同一个,这也算是一个性能优化,帮助渲染页面行为节流。

最后其实咱们这个计算出来的 expirationTime 是能够反推出另一个时间的:

export function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}
复制代码

若是咱们将以前计算出来的 expirationTime 代入以上代码,得出的结果以下:

(1073741822 - 1073741552) * 10 = 2700
复制代码

这个时间其实和咱们以前在上文中计算出来的 2500 毫秒差值很接近。由于 expirationTime 指的就是一个任务的过时时间,React 根据任务的优先级和当前时间来计算出一个任务的执行截止时间。只要这个值比当前时间大就能够一直让 React 延后这个任务的执行,以便让更高优先级的任务执行,可是一旦过了任务的截止时间,就必须让这个任务立刻执行。

这部分的内容一直在算来算去,看起来可能有点头疼。固然若是你嫌麻烦,只须要记住任务的过时时间是经过当前时间加上一个常量(任务优先级不一样常量不一样)计算出来的。

另外其实你还能够在后面的代码中看到更加直观且简单的计算过时时间的方式,可是目前那部分代码尚未被使用起来。

scheduleRootUpdate

当咱们计算出时间之后就会调用 updateContainerAtExpirationTime,这个函数其实没有什么好解析的,咱们直接进入 scheduleRootUpdate 函数就好。

首先咱们会建立一个 update这个对象和 setState 息息相关

// update 对象的内部属性
expirationTime: expirationTime,
tag: UpdateState,
// setState 的第一二个参数
payload: null,
callback: null,
// 用于在队列中找到下一个节点
next: null,
nextEffect: null,
复制代码

对于 update 对象内部的属性来讲,咱们须要重点关注的是 next 属性。由于 update 其实就是一个队列中的节点,这个属性能够用于帮助咱们寻找下一个 update。对于批量更新来讲,咱们可能会建立多个 update,所以咱们须要将这些 update 串联并存储起来,在必要的时候拿出来用于更新 state

render 的过程当中其实也是一次更新的操做,可是咱们并无 setState,所以就把 payload 赋值为 {element} 了。

接下来咱们将 callback 赋值给 update 的属性,这里的 callback 仍是 ReactDom.render 的第三个参数。

而后咱们将刚才建立出来的 update 对象插入队列中,enqueueUpdate 函数内部分支较多且代码简单,这里就再也不贴出代码了,有兴趣的能够自行阅读。函数核心做用就是建立或者获取一个队列,而后把 update 对象入队。

最后调用 scheduleWork 函数,这里开始就是调度相关的内容,这部份内容咱们将在下一篇文章中来详细解析。

总结

以上就是本文的所有内容了,这篇文章其实核心仍是放在了计算时间上,由于这个时间和后面的调度息息相关,最后经过一张流程图总结一下 render 流程两篇文章的内容。

最后

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

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

下一篇文章仍是 render 流程相关的内容。

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

相关文章
相关标签/搜索