资讯专栏INFORMATION COLUMN

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

My_Oh_My / 2605人阅读

摘要:就是,如果你不了解这个的话可以阅读下相关文档,是应用初始化时就会生成的一个变量,值也是,并且这个值不会在后期再被改变。

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

文章相关资料

React 16.8.6 源码中文注释,这个链接是文章的核心,文中的具体代码及代码行数都是依托于这个仓库

热身篇

render 流程(一)

此篇文章内容衔接 render 流程(一),当然不看上一篇文章也没什么问题,因为内容并没有强相关。

现在请大家打开 我的代码 并定位到 react-dom 文件夹下的 src 中的 ReactDOM.js 文件,今天的内容会从这里开始。

ReactRoot.prototype.render

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

大家可以先定位到代码的第 592 行。

大家可以看到,在上述的代码中调用了 unbatchedUpdates 函数,这个函数涉及到的知识其实在 React 中相当重要。

大家都知道多个 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 流程相关的内容。

最后,觉得内容有帮助可以关注下我的公众号 「前端真好玩」咯,会有很多好东西等着你。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6881.html

相关文章

  • 剖析 React 源码render 流程(一)

    摘要:大家可以看到是构造函数构造出来的,并且内部有一个对象,这个对象是本文接下来要重点介绍的对象,接下来我们就来一窥究竟吧。在构造函数内部就进行了一步操作,那就是创建了一个对象,并挂载到了上。下一篇文章还是流程相关的内容。这是我的剖析 React 源码的第二篇文章,如果你没有阅读过之前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。 文章相关资料 React ...

    hiYoHoo 评论0 收藏0
  • react解析: render的FiberRoot(三)

    摘要:查看创建核心函数源码行调用函数创建是相关,不用管源码行这个指的是调用创建,下面我们将会说到对象源码行源码行函数中,首先创建了一个,然后又创建了一个,它们两者还是相互引用。 感谢 yck: 剖析 React 源码解析,本篇文章是在读完他的文章的基础上,将他的文章进行拆解和加工,加入我自己的一下理解和例子,便于大家理解。觉得yck写的真的很棒 。React 版本为 16.8.6,关于源码的...

    muddyway 评论0 收藏0
  • 剖析 React 源码:先热个身

    摘要:我们先来看下这个函数的一些神奇用法对于上述代码,也就是函数来说返回值是。不管你第二个参数的函数返回值是几维嵌套数组,函数都能帮你摊平到一维数组,并且每次遍历后返回的数组中的元素个数代表了同一个节点需要复制几次。这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章: 现在工作中基本都用 React 了,由此想了解下内部原理 市面上 Vue 的源码解读数不胜数,但是反观...

    sean 评论0 收藏0
  • 源码全面剖析 React 组件更新机制

    摘要:把组件看作状态机有限状态机使用来控制本地状态使用来传递状态前面我们探讨了如何映射状态到上初始渲染那么接下来我们谈谈时如何同步状态到上的也就是是如何更新组件的是如何对比出页面变化最小的部分这篇文章会为你解答这些问题在这之前你已经了解了版本内 React 把组件看作状态机(有限状态机), 使用state来控制本地状态, 使用props来传递状态. 前面我们探讨了 React 如何映射状态...

    琛h。 评论0 收藏0
  • React-Redux源码剖析

    摘要:为了能够更好的使用这个工具,今天就对它进行一下源码剖析。它内部的关键代码是在不指定的时候等于,这就意味着的源码剖析到此结束,谢谢观看当然如果指定了剖析就还得继续。好了,源码剖析到此结束,谢谢观看 React-Redux是用在连接React和Redux上的。如果你想同时用这两个框架,那么React-Redux基本就是必须的了。为了能够更好的使用这个工具,今天就对它进行一下源码剖析。 Pr...

    Shimmer 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<