资讯专栏INFORMATION COLUMN

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

KavenFan / 2671人阅读

摘要:在学习源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。说明就算抛出了错误,部分的代码也要继续执行,随后再将错误往上层代码抛。和都能保证其中一个成员抛出错误的时候,余下的能继续执行。

前言

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:

 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 件事情:

执行初始化方法 initializeAll

执行传入的 callback 方法

执行收尾方法 closeAll

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

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

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 是怎么用的:

// 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

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注入进去。

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

_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 是怎么被调用的。

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

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

相关文章

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

    摘要:前言是一个十分庞大的库,由于要同时考虑和,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过程。在学习源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常...

    airborne007 评论0 收藏0
  • React 源码深度解读(四):首次自定义组件渲染 - Part 1

    摘要:本篇开始介绍自定义组件是如何渲染的。组件将自定义组件命名为,结构如下经过编译后,生成如下代码构建顶层包装组件跟普通元素渲染一样,第一步先会执行创建为的。调用顺序已在代码中注释。先看图,这部分内容将在下回分解 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非...

    Warren 评论0 收藏0
  • React 源码深度解读(三):首次 DOM 元素渲染 - Part 3

    摘要:在学习源码的过程中,给我帮助最大的就是这个系列文章,于是决定基于这个系列文章谈一下自己的理解。到此为止,首次渲染就完成啦总结从启动到元素渲染到页面,并不像看起来这么简单,中间经历了复杂的层级调用。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非常艰辛的过...

    U2FsdGVkX1x 评论0 收藏0
  • React 源码深度解读(六):依赖注入

    摘要:依赖注入和控制反转,这两个词经常一起出现。一句话表述他们之间的关系依赖注入是控制反转的一种实现方式。而两者有大量的代码都是可以共享的,这就是依赖注入的使用场景了。下一步就是创建具体的依赖内容,然后注入到需要的地方这里的等于这个对象。 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级...

    glumes 评论0 收藏0
  • React 源码深度解读(二):首次 DOM 元素渲染 - Part 2

    摘要:本文将要讲解的调用栈是下面这个样子的平台无关相关如果看源码,我们会留意到很多相关的代码,我们暂时先将其忽略,会在后续的文章中进行讲解。现在我们来看一下各实例间的关系目前为止的调用栈平台无关相关下一篇讲解三总结本文讲解了转化为的过程。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 R...

    wean 评论0 收藏0

发表评论

0条评论

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