React源码解析(三):详解事务与队列

综合技术 稀土掘金 (源链)

笔者将编写”React源码解析”系列文章三到四篇,阐述React内部的机制。欢迎大家关注我的掘金账号,以便能及时看到最新的文章更新推送。

在前两篇文章中,我们分析了React组件的实现,挂载以及生命周期的流程。在阅读源码的过程中,我们经常会看到诸如 transactionUpdateQueue 这样的代码,这涉及到React中的两个概念:事务和队列。因为之前的文章对于这些我们一笔带过,所以本篇我们基于大家都再熟悉不过的 setState 方法来探究事务机制和更新队列。

1.setState的实现

在第一篇文章 《React源码解析(一):组件的实现与挂载》 中我们已经知道,通过 class 声明的组件具有 setState 方法:


该方法传入两个参数 partialStatecallBack ,前者是新的state值,后者是回调函数。而 updater 在构造函数中已经被定义过:


可以看出 updater 是构造函数传入的,所以找到哪里执行了 new ReactComponent ,就能找到 updater 是什么。以自定义组件 ReactCompositeComponent 为例,在 _constructComponentWithoutOwner 方法中,我们发现了它的踪迹:

return new Component(publicProps, publicContext, updateQueue);

可以看到 updater 其实就是 updateQueue 。接下来我们看看 enqueueSetState 是什么:


getInternalInstanceReadyForUpdate 方法的目的是获取当前组件对象,将其赋值给 internalInstance 变量。接下来判断当前组件对象的state更新队列是否存在,如果存在则将 partialState 也就是新的state值加入队列;如果不存在,则创建该对象的更新队列,可以注意到队列是以数组形式存在的。我们再看下 enqueueUpdate 方法做了哪些事:


由代码可见,当 isBatchingUpdatesfalse 时,将执行 batchedUpdates 更新队列,若为 true 时,则将组件放入 dirtyComponent 中。那么 batchingStrategy.batchedUpdates() 又是何方神圣呢?



其中 isBatchingUpdates 布尔值会根据当前队列所处阶段来更改,或者说是一个标志位。而在最后我们终于看到了 transaction.perform() ,接下来我们进入本文的第二部分:事务。

2.transaction事务

首先看下源码中的解析图:

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * 

从流程图上看很简单,每一个方法会被 wrapper 所包裹,必须用 perform 调用,在被包裹方法前后分别执行 initializeclose 。举例如下:


那么在前面的代码中 transaction.perform(callBack) 实际调用的是 transaction.perform(enqueueUpdate) ,但 enqueueUpdate 方法中仍然存在 transaction.perform(enqueueUpdate) ,这样岂不是造成了死循环?

我们在前面的代码可以看到, transaction 中注册了 RESET_BATCHED_UPDATESFLUSH_BATCHED_UPDATES


在最初执行 enqueueUpdate 时, isBatchingUpdates 初始值为 false (为描述方便下文简称为 U ),判断后会执行 batchingStrategy.batchedUpdates() ,在 batchUpdates 方法中又会将 U 设置为 true ,表明当前进入更新状态。一旦 U 值为 true ,再进入 enqueueUpdate 函数,将不会再执行 batchingStrategy.batchedUpdates() ,而是执行 dirtyComponents.push(component) ,故不会造成死循环。

当流程全部完成后, RESET_BATCHED_UPDATES 中的 close 方法会再次将 U 设置为 false 。上述过程可参照下面的流程图:(点击可以查看大图)


从上面的分析我们已经知道, RESET_BATCHED_UPDATES 是用于更改 U 的状态 false 或者 true ,那 FLUSH_BATCHED_UPDATES 的作用是什么呢?


可以看到 flushBatchedUpdates 方法循环遍历所有的 dirtyComponents ,并在 close 阶段执行 runBatchedUpdates 方法:


该方法主要做两件事,一是通过执行 performUpdateIfNecessary 方法来更新组件,二是若有 setState 的回调函数则执行回调函数。

再看下 performUpdateIfNecessary


因为 internalInstanceReactCompositeComponent 的实例,所以我们看下代码:


在上述代码中,因为我们以更新组件而不是改变组件为例,故接下来会执行 this.updateComponent :


可以看到执行了 componentWillReceiveProps 方法和 shouldComponentUpdate 方法。其中不能忽视的一点是在 shouldComponentUpdate 之前,执行了 _processPendingState 方法,我们看下这个函数做了什么:


该函数主要对 state 进行处理:

1.如果更新队列为 null ,那么返回原来的 state

2.如果更新队列有一个更新,那么返回更新值;

3.如果更新队列有多个更新,那么通过for循环将它们合并;

综上说明了,在一个生命周期内,在 componentShouldUpdate 执行之前,所有的 state 变化都会被合并,最后统一处理。

回到 _updateComponent ,最后如果 shouldUpdatetrue ,执行 _performComponentUpdate 方法:


大致浏览下会发现还是同样的套路,执行 componentWillUpdate 方法,进行更新,最后执行 componentDidUpdate 方法。我们看下负责更新的 _updateRenderedComponent 方法:


这段代码的思路就很清晰了:

  1. 获取旧的组件信息
  2. 获取新的组件信息
  3. shouldUpdateReactComponent 是一个方法(下文简称 should 函数),根据传入的新旧组件信息判断是否进行更新。
  4. should 函数返回 true ,执行旧组件的更新。
  5. should 函数返回 false ,执行旧组件的卸载和新组件的挂载。

如下图所示(点击可以查看大图):


3.梳理

说了这么多有点头晕了, setState 设计得复杂而又有条理,我们从头把脉络再整理一遍,先看下关键词:

关键词功能
_pendingStateQueue state 更新队列,更新完成后置为 null
dirtyComponents内部数组,用于存储准备更新的组件
transaction.performReact内部事务调用
_processPendingState 内部函数,用于处理新 state 的合并
_updateRenderedComponent内部函数,用于处理组件的更新或卸载

最后上图:(点击可以查看大图)


4.写在最后

(1) setState 回调函数

setState 回调函数与 state 的流程相似, stateenqueueSetState 处理,回调函数由 enqueueCallback 处理,本文不再赘述。

(2)关于 setState 导致的崩溃问题

我们已经知道, this.setState 实际调用了 enqueueSetState ,在进一步对 dirtyComponent 进行批量处理的时候,因为 _pendingStateQueue 还未进行合并处理,故在下面 performUpdateIfNecessary 代码中:


因为更新逻辑执行的是 this.updateComponent 的流程,而后在合并state的时候:


会将 this._pendingStateQueue 设置为 null ,这样 dirtyComponent 进入下一次批量处理时,已经更新过的组件不会进入重复的流程,保证组件只做一次更新操作。

同理,之所以不能在 componentWillUpdate 中调用 setState 的原因,就是 setState 会触发 _pendingStateQueue !== null ,导致再次执行 updateComponent ,而后会再次调用 componentWillUpdate ,最终循环调用 componentWillUpdate 导致浏览器的崩溃。

(3)关于React依赖注入

我们在之前的代码中,对于更新队列的标志 batchingStrategy ,我们直接转向对 ReactDefaultBatchingStrategy 进行分析,这是因为React内部存在大量的依赖注入。在React初始化时, ReactDefaultInjection.js 注入到 ReactUpdates 中作为默认的strategy:


依赖注入在React的服务端渲染中有大量的应用,有兴趣的同学可以自行探索。

回顾:

您可能感兴趣的

一个用于为子组件添加三维特效的 React 组件... 该组件主要用于对其子组件添加三维效果,包括摄像机的放大缩小、摄像机的旋转、子组件的平移、子组件选中以及子组件的层叠和栅格布局,所有变换中,子组件的法向量保持不变,即始终指向屏幕外。所有的三维效果都使用 CSS3 中的 transform 来实现。本文将详细介绍一下该组件的实现细节。 Gith...
听听大家怎么评论:VueJS 对比 ReactJS 新的框架层出不穷,一个人很难跟上所有这些库和框架,也就是说,这就需要您决定哪些是值得花时间的。让我们看看大家怎么看待VueJS 和 ReactJS的。 Reme Le Hane “比起React我更喜欢vue.js。在JavaScript领域vue.js...
Formik: Handle forms in React, without tears Forms in React, without tears. Let's face it, forms are really really verbose in React. To make matters worse, most form helpers do wayyyyy t...
飞冰 ICE —— ​基于 React 的中后台应用解决方案... 飞冰(ICE) 是一套基于 React 的中后台应用解决方案,ICE 包含了一条从设计端到开发端的完整链路,帮助用户快速搭建属于自己的中后台应用。 面向设计者端,ICE 提供了 ICE Design 设计语言,来给 UI 界面提供专业的视觉指导。面向开发者端,ICE 提供了 Iceworks 工...
基于React的大文件上传组件的开发详解 本文出自 perkin 以前实习的时候有做过大文件上传的需求,当时我们团队用的是网宿科技的存储服务,自然而然用的也是他们上传的js-sdk,不管是网宿科技还是七牛等提供存储服务的公司,他们的文件上传底层使用的基本上都是plupload库。除了这个,百度FEX团队开源的webuploader...
稀土掘金责编内容来自:稀土掘金 (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » React源码解析(三):详解事务与队列



专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录