React源码分析 - 组件更新与事务

在React中,组件的更新本质上都是由setState操作改变state引起的。因此组件更新的入口在于setState,同样经过撸源码和打断点分析画了以下的组件更新的流程图:

setState的定义在组件mountComponent的时候定义:

inst = new Component(publicProps, publicContext, ReactUpdateQueue);
复制代码
function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
复制代码

所以setState的真正的定义在 ReactUpdateQueue.js

enqueueSetState: function (publicInstance, partialState) {
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  if (!internalInstance) {
    return;
  }
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  // 将state添加到对应的component的_pendingStateQueue数组中。
  queue.push(partialState);
  enqueueUpdate(internalInstance);
}

enqueueCallback: function (publicInstance, callback, callerName) {
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
  if (!internalInstance) {
    return null;
  }
  // 将callback添加到对应的component的_pendingCallbacks数组中。
  if (internalInstance._pendingCallbacks) {
    internalInstance._pendingCallbacks.push(callback);
  } else {
    internalInstance._pendingCallbacks = [callback];
  }
  enqueueUpdate(internalInstance);
}
复制代码

两个方法最后都调用enqueueUpdate:

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

function enqueueUpdate(component) {
  // 确认需要的事务是否注入了。
  ensureInjected();
  // batchingStrategy.isBatchingUpdates为false的时候,
  // 或者说当不处于批量更新的时候,用事务的方式批量的进行component的更新。
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 当处于批量更新阶段时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中
  dirtyComponents.push(component);
}
复制代码

这里需要注意enqueueUpdate中根据batchingStrategy.isBatchingUpdates分别进入不同的流程,当isBatchingUpdates为true的时候表示已经处于批量更新的过程中了,这时候会将所有的有改动的组件push到dirtyComponents中。当isBatchingUpdates为false的时候会执行更新操作,这里先认为当isBatchingUpdates为false的时候进行的操作是更新组件,实际上的过程是更复杂的,稍后马上解释具体的过程。这里我们先理解下React中的事务的概念,事务的概念根据源码中的注释就可以非常清楚的了解了:

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

简单来说当使用transaction.perform执行方法method的时候会按顺序先执行WRAPPER里面的initialize方法然后执行method最后再执行close方法。

React提供了基础的事务对象Transaction,不同的事务的区别就在于initialize和close方法的不同,这个可以通过定义getTransactionWrappers方法来传入WRAPPER数组,具体的用法看下源码就好了,不过实际使用中是不会要自己去定义事务的,当然要的话也阻止不了~。

回到enqueueUpdate,其调用的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定义了:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      callback(a, b, c, d, e);
    } else {
      transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};
复制代码

可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程。

对于enqueueUpdate的效果就是,当执行enqueueUpdate的时候如果isBatchingUpdates为true的话(已经处于批量执行操作),则不会进行更新操作,而是将改动的component添加到dirtyComponents数组中;如果isBatchingUpdates为false的话,会执行batchedUpdates将isBatchingUpdates置为true然后调用enqueueUpdate方法,这个时候会用事务的方式来执行enqueueUpdate。

根据流程图可以知道,事务ReactDefaultBatchingStrategyTransaction的initialize是foo没有任务操作,接着会执行method即:将改动的组件push到dirtyComponent中,最后执行close方法执行flushBatchedUpdate方法再把isBatchingUpdates重置为false。在flushBatchedUpdates方法中事务执行runBatchedUpdates方法将dirtyComponent中的component依次(先父组件在子组件的顺序)进行更新操作。这里具体的更新的过程看流程图就可以理解了,需要注意的是updateChildren方法这个方法是virtual DOM的Diff算法的核心代码,作用就是根据更新前后组件的不同进行有效的更新,具体的部分,之后单独的文章再介绍。

在更新的过程中需要注意的一个方法是_processPendingState方法:

_processPendingState: function (props, context) {
   var inst = this._instance;
   var queue = this._pendingStateQueue;
   var replace = this._pendingReplaceState;
   this._pendingReplaceState = false;
   this._pendingStateQueue = null;

   if (!queue) {
     return inst.state;
   }

   if (replace && queue.length === 1) {
     return queue[0];
   }

   var nextState = _assign({}, replace ? queue[0] : inst.state);
   for (var i = replace ? 1 : 0; i < queue.length; i++) {
     var partial = queue[i];
     _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
   }

   return nextState;
 }
复制代码

可以看到当setState传入的是函数的时候,函数被调用的时候的传入的参数是merge了已经遍历的queue的state的nextState,如果传入的不是函数则直接merge state至nextState。这也解释了,为什么用回调函数的形式使用setState的时候可以解决state是按照顺序最新的state了。

从流程图可以看到在保证组件更新完毕后会将setState中传入的callback按照顺序依次push到事务的callback queue队列中,在事务结束的时候close方法中notifyAll就是执行这些callbacks,这样保证了回调函数是在组件完全更新完成后执行的,也就是setState的回调函数传入的state是更新后的state的原因。

在了解了以上的组件更新的流程后,可以看一个场景,栗子如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

import React, { Component } from 'react';
import Hello from './Hello';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      appText: 'hello App',
      helloText: 'heiheihei'
    };
  }

  handleAppClick = () => {
    console.log('App is clicked ~');
    this.setState({
      appText: 'App is clicked ~'
    });
  }

  render() {
    const { appText, helloText } = this.state;
    console.log('render App');
    return (
      <div className="app-container">
        <div          
          onClick={this.handleAppClick}
        >{appText}</div>
        <Hello
          text={helloText}
          handleAppClick={this.handleAppClick}
        />
      </div>
    );
  }
}

export default App;

import React, { Component } from 'react';

class Hello extends Component {
    constructor(props) {
        super(props);
        this.state = {
            text: 'hello Hello'
        };
    }

    componentWillReceiveProps(nextProps) {
        this.setState({
          text: nextProps.text + '~'
        });
    }

    handleClick = () => {
        this.setState({
            text: 'Hello is clicked ~'
        });
        this.props.handleAppClick();
    }

    render() {
        const { text } = this.state;
        console.log('render Hello');
        return (
            <div>
                <div
                    onClick={this.handleClick}
                    style={{ color: '#e00' }}
                >{text}</div>
            </div>
        );
    }
}

export default Hello;
复制代码

点击

hello Hello
复制代码

后组件的渲染如下,可以看到父组件到子组件按顺序更新了一次:

render App
render Hello
复制代码

而不是:

render Hello
render App
render Hello
复制代码

批量更新的时候组件的顺序由:

dirtyComponents.sort(mountOrderComparator);
复制代码

处理的。

到这里你需要知道这个结果产生的原因在于不是只有setState的调用栈会改变isBatchingUpdates的值

回顾《React事件机制》的流程图可以知道事件的统一回调函数dispatchEvent调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理,也就是说点击事件的处理本身就是在一个大的事务中,在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中,在事务结束的时候按照上述的流程进行批量更新,然后将批量执行关闭结束事务。

事务的机制在这里也保证了React更新的效率,此外在更新组件的时候的virtual DOM的Diff算法也起到很大的作用,这个在后续的文章再介绍。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值