React 源码深度解读(七):事务 - Part 1

2022-06-14 13:34:52 浏览数 (1)

  • 前言

React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程。在学习 React 源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。本文会大量用到原文中的例子,想体会原汁原味的感觉,推荐阅读原文。

本系列文章基于 React 15.4.2 ,以下是本系列其它文章的传送门:

React 源码深度解读(一):首次 DOM 元素渲染 - Part 1

React 源码深度解读(二):首次 DOM 元素渲染 - Part 2

React 源码深度解读(三):首次 DOM 元素渲染 - Part 3

React 源码深度解读(四):首次自定义组件渲染 - Part 1

React 源码深度解读(五):首次自定义组件渲染 - Part 2

React 源码深度解读(六):依赖注入

React 源码深度解读(七):事务 - Part 1

React 源码深度解读(八):事务 - Part 2

React 源码深度解读(九):单个元素更新

React 源码深度解读(十):Diff 算法详解

  • 正文

在阅读 React 源码过程中,transaction 可以说无处不在,所有涉及到 UI 更新相关的操作都会借助 transaction 来完成。下面,我们就来看看它所起到的特殊所用。

  • Transaction 核心实现

Transaction 本质来说只是一个对象,它的核心方法是 perform:

代码语言:javascript复制
 perform: function <
        A,
        B,
        C,
        D,
        E,
        F,
        G,
        T: (a: A, b: B, c: C, d: D, e: E, f: F) => G // eslint-disable-line space-before-function-paren
            >
        (
            method: T, scope: any,
            a: A, b: B, c: C, d: D, e: E, f: F,
        ): G {
            var errorThrown;
            var ret;
            try {
                this._isInTransaction = true;
                // Catching errors makes debugging more difficult, so we start with
                // errorThrown set to true before setting it to false after calling
                // close -- if it's still set to true in the finally block, it means
                // one of these calls threw.
                errorThrown = true;
                this.initializeAll(0);
                ret = method.call(scope, a, b, c, d, e, f);
                errorThrown = false;
            } finally {
                try {
                    if (errorThrown) {
                        // If `method` throws, prefer to show that stack trace over any thrown
                        // by invoking `closeAll`.
                        try {
                            this.closeAll(0);
                        } catch (err) {}
                    } else {
                        // Since `method` didn't throw, we don't want to silence the exception
                        // here.
                        this.closeAll(0);
                    }
                } finally {
                    this._isInTransaction = false;
                }
            }
            return ret;
        },

可以看到,这个方法只做了 3 件事情:

  1. 执行初始化方法 initializeAll
  2. 执行传入的 callback 方法
  3. 执行收尾方法 closeAll

这里的结构很有意思,有 try 竟然没有 catch,取而代之的是 finally。说明就算抛出了错误,finally 部分的代码也要继续执行,随后再将错误往上层代码抛。这样能保证无论在什么情况下,closeAll 都能得到执行。

下面来看一下结构极其相似的 initializeAll 和 closeAll 方法:

代码语言:javascript复制
initializeAll: function (startIndex: number): void {
    var transactionWrappers = this.transactionWrappers;
    
    for (var i = startIndex; i < transactionWrappers.length; i  ) {
        var wrapper = transactionWrappers[i];
        try {
            // Catching errors makes debugging more difficult, so we start with the
            // OBSERVED_ERROR state before overwriting it with the real return value
            // of initialize -- if it's still set to OBSERVED_ERROR in the finally
            // block, it means wrapper.initialize threw.
            this.wrapperInitData[i] = OBSERVED_ERROR;
            this.wrapperInitData[i] = wrapper.initialize ?
                wrapper.initialize.call(this) :
                null;
        } finally {
            if (this.wrapperInitData[i] === OBSERVED_ERROR) {
                // The initializer for wrapper i threw an error; initialize the
                // remaining wrappers but silence any exceptions from them to ensure
                // that the first error is the one to bubble up.
                try {
                    this.initializeAll(i   1);
                } catch (err) {}
            }
        }
    }
},

...

closeAll: function (startIndex: number): void {
    var transactionWrappers = this.transactionWrappers;
    
    for (var i = startIndex; i < transactionWrappers.length; i  ) {
        var wrapper = transactionWrappers[i];
        var initData = this.wrapperInitData[i];
        var errorThrown;
        try {
            // Catching errors makes debugging more difficult, so we start with
            // errorThrown set to true before setting it to false after calling
            // close -- if it's still set to true in the finally block, it means
            // wrapper.close threw.
            errorThrown = true;
            if (initData !== OBSERVED_ERROR && wrapper.close) {
                wrapper.close.call(this, initData);
            }
            errorThrown = false;
        } finally {
            if (errorThrown) {
                // The closer for wrapper i threw an error; close the remaining
                // wrappers but silence any exceptions from them to ensure that the
                // first error is the one to bubble up.
                try {
                    this.closeAll(i   1);
                } catch (e) {}
            }
        }
    }
    this.wrapperInitData.length = 0;
},

transactionWrappers 是一个数组,一个 transaction 可以有多个 wrapper,通过 reinitializeTransaction 来初始化。每个 wrapper 都需要定义 initialize 和 close 方法。initializeAll 和 closeAll 都能保证其中一个 wrapper 成员抛出错误的时候,余下的 wrapper 能继续执行。initialize 有一个返回值,传给对应的 close 方法。当 initialize 抛出错误的时候,由于没有 catch,exception 会一直往上抛,中断了ret = method.call(scope, a, b, c, d, e, f)的执行去到 finally,接着执行 closeAll。

了解 transaction 的基本概念后,我们来看下它是怎么应用的。

  • ReactDefaultBatchingStrategyTransaction

我们以ReactDefaultBatchingStrategyTransaction为例子来看看 transaction 是怎么用的:

代码语言:javascript复制
// transaction 子类
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

// 覆盖 transaction 的 getTransactionWrappers 方法
Object.assign(
  ReactDefaultBatchingStrategyTransaction.prototype,
  Transaction,
  {
    getTransactionWrappers: function() {
      return TRANSACTION_WRAPPERS;
    },
  }
);

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

首先,ReactDefaultBatchingStrategyTransaction继承了 transaction,并覆盖了getTransactionWrappers这个方法来定义自己的 wrapper。这 2 个 wrapper 很简单,initialize都是空函数,close 的时候就重置下标志位,然后再调用另一个方法。

下面再看一下创建ReactDefaultBatchingStrategyTransaction的对象ReactDefaultBatchingStrategy

代码语言:javascript复制
var transaction = new ReactDefaultBatchingStrategyTransaction();

var ReactDefaultBatchingStrategy = {
    isBatchingUpdates: false,

    /**
     * Call the provided function in a context within which calls to `setState`
     * and friends are batched such that components aren't updated unnecessarily.
     */
    batchedUpdates: function (callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

        ReactDefaultBatchingStrategy.isBatchingUpdates = true;

        // The code is written this way to avoid extra allocations
        if (alreadyBatchingUpdates) {
            return callback(a, b, c, d, e);
        } else {
            return transaction.perform(callback, null, a, b, c, d, e);
        }
    },
};

第一步是创建一个 ReactDefaultBatchingStrategyTransaction 实例。当batchedUpdates第一次被调用的时候,alreadyBatchingUpdates为 false,会调用transaction.perform,让后续的操作都处于 transaction 的上下文之中。后面再调用batchedUpdates的时候,只是单纯的执行callback

而调用ReactDefaultBatchingStrategy的是ReactUpdates,它通过依赖注入的方法在运行的时候将ReactDefaultBatchingStrategy注入进去。

代码语言:javascript复制
function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber   1;
  }
}

enqueueUpdate第一次执行的时候,它会检测是否在 batchUpdate 的模式下(batchingStrategy.isBatchingUpdates),如果不是则调用batchingStrategy.batchedUpdates,如果是则执行dirtyComponents.push(component)

当我们使用setState的时候,它会调用ReactUpdatesenqueueSetState,然后再调用enqueueUpdate。如果在 React 的生命周期函数又或者使用 React 自带的合成事件时,会在setState之前先将整个处理过程设置为 batchUpdate 的模式,所以当我们setState的时候,实际上只会执行dirtyComponents.push(component),并不会马上更新 state,这就是为什么setState看似异步更新的原因。实际上它还是同步的。

以 React 生命周期函数为例子,当 Component 被初始化的时候,会执行_renderNewRootComponent

代码语言:javascript复制
_renderNewRootComponent: function (
    nextElement,
    container,
    shouldReuseMarkup,
    context
) {
    ...

    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.

    ReactUpdates.batchedUpdates(
        batchedMountComponentIntoNode,
        componentInstance,
        container,
        shouldReuseMarkup,
        context
    );

    ...
},

在这里就预先将整个处理过程设置为 batchUpdate 的模式了,官方的注释也说明了这点。

  • 总结

我们再通过一张图,来总结下 transaction 是怎么被调用的。

0 人点赞