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.perform React内部事务调用
_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的服务端渲染中有大量的应用,有兴趣的同学可以自行探索。

回顾:

稀土掘金稿源:稀土掘金 (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » React源码解析(三):详解事务与队列

喜欢 (0)or分享给?

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

使用声明 | 英豪名录