React源码分析与实现(二):状态、属性更新 -> setState

原文链接地址:https://github.com/Nealyang 转载请注明出处

状态更新

此次分析setState基于0.3版本,实现比较简单,后续会再分析目前使用的版本以及事务机制。

流程图大概如下

IMAGE

setState的源码比较简单,而在执行更新的过程比较复杂。我们直接跟着源码一点一点屡清楚。

  • ReactCompositeComponent.js
  /**
   * Sets a subset of the state. Always use this or `replaceState` to mutate
   * state. You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * @param {object} partialState Next partial state to be merged with state.
   * @final
   * @protected
   */
  setState: function(partialState) {
    // Merge with `_pendingState` if it exists, otherwise with existing state.
    this.replaceState(merge(this._pendingState || this.state, partialState));
  },

注释部分说的很明确,setState后我们不能够立即拿到我们设置的值。

而这段代码也非常简单,就是将我们传入的state和this._pendingState做一次merge,merge的代码在util.js下

var merge = function(one, two) {
  var result = {};
  mergeInto(result, one);
  mergeInto(result, two);
  return result;
};

function mergeInto(one, two) {
  checkMergeObjectArg(one);
  if (two != null) {
    checkMergeObjectArg(two);
    for (var key in two) {
      if (!two.hasOwnProperty(key)) {
        continue;
      }
      one[key] = two[key];
    }
  }
}

  checkMergeObjectArgs: function(one, two) {
    mergeHelpers.checkMergeObjectArg(one);
    mergeHelpers.checkMergeObjectArg(two);
  },

  /**
   * @param {*} arg
   */
  checkMergeObjectArg: function(arg) {
    throwIf(isTerminal(arg) || Array.isArray(arg), ERRORS.MERGE_CORE_FAILURE);
  },
  
  var isTerminal = function(o) {
  return typeof o !== 'object' || o === null;
};

var throwIf = function(condition, err) {
  if (condition) {
    throw new Error(err);
  }
};

诊断代码的逻辑非常简单,其实功能就是Object.assign() ,但是从上面代码我们可以看出react源码中的function大多都具有小而巧的特点。

最终,将merge后的结果传递给replaceState

replaceState: function(completeState) {
    var compositeLifeCycleState = this._compositeLifeCycleState;
    invariant(
      this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
      compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
      'replaceState(...): Can only update a mounted (or mounting) component.'
    );
    invariant(
      compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
      compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
      'replaceState(...): Cannot update while unmounting component or during ' +
      'an existing state transition (such as within `render`).'
    );

    this._pendingState = completeState;

    // Do not trigger a state transition if we are in the middle of mounting or
    // receiving props because both of those will already be doing this.
    if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
        compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

      var nextState = this._pendingState;
      this._pendingState = null;

      var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
      transaction.perform(
        this._receivePropsAndState,
        this,
        this.props,
        nextState,
        transaction
      );
      ReactComponent.ReactReconcileTransaction.release(transaction);

      this._compositeLifeCycleState = null;
    }
  },

撇开50% 判断warning代码不说,从上面代码我们可以看出,只有在componsiteLifeState不等于mounting和receiving_props 时,才会调用 _receivePropsAndState函数来更新组件。

我们可以演示下:

var ExampleApplication = React.createClass({
      getInitialState() {
        return {}
      },
      componentWillMount() {
        this.setState({
          a: 1,
        })
        console.log('componentWillMount', this.state.a)
        this.setState({
          a: 2,
        })
        console.log('componentWillMount', this.state.a)
        this.setState({
          a: 3,
        })
        console.log('componentWillMount', this.state.a)
        setTimeout(() => console.log('a5'), 0)
        setTimeout(() => console.log(this.state.a,'componentWillMount'))

        Promise.resolve('a4').then(console.log)
      },

      componentDidMount() {
        this.setState({
          a: 4,
        })
        console.log('componentDidMount', this.state.a)
        this.setState({
          a: 5,
        })
        console.log('componentDidMount', this.state.a)
        this.setState({
          a: 6,
        })
        console.log('componentDidMount', this.state.a)
      },
      render: function () {
        var elapsed = Math.round(this.props.elapsed / 100);
        var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0');
        var message =
          'React has been successfully running for ' + seconds + ' seconds.';
        return React.DOM.p(null, message);
      }
    });
IMAGE

所以以上结果我们可以看出,在componentWillMount生命周期内setState后this.state不会改变,在componentDidMount是正常的。因为在上一篇文章中我们也有说到,在mountComponent过程中,会把compositeLifeCycleState设置为MOUNTING状态,在这个过程中,是不会执行receivePropsAndState的,所以this.state也就不会更新,同理,在receivePropsAndState的过程中,会把compositeLifeCycleState置成RECEIVING_PROPS状态,也不会执行state更新以及render执行,在updateComponent过程中又执行了mountComponent函数,mountComponent函数调用了render函数。

而在现在我们使用16或者15版本中,我们发现:

componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

最后打印的结果为:0,0,2,3

IMAGE

为什么有这样呢?其实源于源码中的这段代码:

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 setProps, 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);
}

因为这里涉及到事务的概念、批量更新以及benchUpdate等,在我们目前分析的版本中还未迭代上去,后面我们会跟着版本升级慢慢说道。

img

属性更新

首先我们知道,属性的更新必然是由于state的更新,所以其实组件属性的更新流程就是setState执行更新的延续,换句话说,也就是setState才能出发组件属性的更新,源码里就是我在处理state更新的时候,顺带检测了属性的更新。所以这段源码的开始,还是从setState中看

  _receivePropsAndState: function(nextProps, nextState, transaction) {
    if (!this.shouldComponentUpdate ||
        this.shouldComponentUpdate(nextProps, nextState)) {
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      this.props = nextProps;
      this.state = nextState;
    }
  },

代码非常的简单,一句话解释:当shouldComponentUpdate为true时,则执行更新操作。

  _performComponentUpdate: function(nextProps, nextState, transaction) {
    var prevProps = this.props;
    var prevState = this.state;

    if (this.componentWillUpdate) {
      this.componentWillUpdate(nextProps, nextState, transaction);
    }

    this.props = nextProps;
    this.state = nextState;

    this.updateComponent(transaction);

    if (this.componentDidUpdate) {
      transaction.getReactOnDOMReady().enqueue(
        this,
        this.componentDidUpdate.bind(this, prevProps, prevState)
      );
    }
  },

这段代码的核心就是调用this.updateComponent,然后对老的属性和状态存一下,新的更新一下而已。如果存在componentWillUpdate就执行一下,然后走更新流程。最后是把执行componentDidUpdate推入getReactOnDOMReady的队列中,等待组件的更新。

  _renderValidatedComponent: function() {
    ReactCurrentOwner.current = this;
    var renderedComponent = this.render();
    ReactCurrentOwner.current = null;
    return renderedComponent;
  },
  ...
  ...
  updateComponent: function(transaction) {
    var currentComponent = this._renderedComponent;
    var nextComponent = this._renderValidatedComponent();
    if (currentComponent.constructor === nextComponent.constructor) {
      if (!nextComponent.props.isStatic) {
        currentComponent.receiveProps(nextComponent.props, transaction);
      }
    } else {
      var thisID = this._rootNodeID;
      var currentComponentID = currentComponent._rootNodeID;
      currentComponent.unmountComponent();
      var nextMarkup = nextComponent.mountComponent(thisID, transaction);
      ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
        currentComponentID,
        nextMarkup
      );
      this._renderedComponent = nextComponent;
    }
  },

这里我们直接看updateComponent更新流程,首先获取当前render函数的组件,然后获取下一次render函数的组件,_renderValidatedComponent就是获取下一次的render组件。 通过Constructor来判断组件是否相同,如果相同且组件为非静态,则更新组件的属性,否则卸载当前组件,然后重新mount下一个render组件并且直接暴力更新。

接着会调用render组件的receiveProps方法,其实一开始这个地方我也是非常困惑的,this指向傻傻分不清楚,后来经过各种查阅资料知道,它其实是一个多态方法,如果是复合组件,则执行ReactCompositeComponent.receiveProps,如果是原生组件,则执行ReactNativeComponent.receiveProps。源码分别如下:

  receiveProps: function(nextProps, transaction) {
    if (this.constructor.propDeclarations) {
      this._assertValidProps(nextProps);
    }
    ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);

    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
    if (this.componentWillReceiveProps) {
      this.componentWillReceiveProps(nextProps, transaction);
    }
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
    var nextState = this._pendingState || this.state;
    this._pendingState = null;
    this._receivePropsAndState(nextProps, nextState, transaction);
    this._compositeLifeCycleState = null;
  },

有人可能注意到这里的this._receivePropsAndState函数,这不是刚才调用过么?怎么又调用一遍?没错,调用这个的this已经是currentComponent了,并不是上一个this。currentComponent是当前组件的render组件,也就是当前组件的子组件。子组件同样也可能是复合组件或者原生组件。正式通过这种多态的方式,递归的解析每级嵌套组件。最终完成从当前组件到下面的所有叶子节点的树更新。

其实话说回来,compositeComponent最终还是会遍历递归到解析原生组件,通过我们整体浏览下ReactNativeComponent.js代码可以看出。

IMAGE

我们先从 receiveProps方法开始看

  receiveProps: function(nextProps, transaction) {
    assertValidProps(nextProps);
    ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
    this._updateDOMProperties(nextProps);
    this._updateDOMChildren(nextProps, transaction);
    this.props = nextProps;
  },
  
  function assertValidProps(props) {
  if (!props) {
    return;
  }
  var hasChildren = props.children != null ? 1 : 0;
  var hasContent = props.content != null ? 1 : 0;
  var hasInnerHTML = props.dangerouslySetInnerHTML != null ? 1 : 0;
}

删除安全警告和注释其实代码非常简答,首先assertValidProps就是校验props是否合法的,更新属性的方法就是_updateDOMProperties

_updateDOMProperties: function(nextProps) {
    var lastProps = this.props;
    for (var propKey in nextProps) {
      var nextProp = nextProps[propKey];
      var lastProp = lastProps[propKey];
      //判断新老属性中的值是否相等
      if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
        continue;
      }
      //如果是style样式,遍历新style,如果去旧style不相同,则把变化的存入styleUpdates对象中。最后调用 updateStylesByID 统一修改dom的style属性。
      if (propKey === STYLE) {
        if (nextProp) {
          nextProp = nextProps.style = merge(nextProp);
        }
        var styleUpdates;
        for (var styleName in nextProp) {
          if (!nextProp.hasOwnProperty(styleName)) {
            continue;
          }
          if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
            if (!styleUpdates) {
              styleUpdates = {};
            }
            styleUpdates[styleName] = nextProp[styleName];
          }
        }
        if (styleUpdates) {
          ReactComponent.DOMIDOperations.updateStylesByID(
            this._rootNodeID,
            styleUpdates
          );
        }
      } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
        var lastHtml = lastProp && lastProp.__html;
        var nextHtml = nextProp && nextProp.__html;
        if (lastHtml !== nextHtml) {
          ReactComponent.DOMIDOperations.updateInnerHTMLByID(//注意这里是innerHtml,所以dangerouslyInnerHTML会展示正常的HTML
            this._rootNodeID,
            nextProp
          );
        }
      } else if (propKey === CONTENT) {
        ReactComponent.DOMIDOperations.updateTextContentByID(//这里是innerText,所以content与children原封不动的把HTML代码打印到页面上
          this._rootNodeID,
          '' + nextProp
        );
      } else if (registrationNames[propKey]) {
        putListener(this._rootNodeID, propKey, nextProp);
      } else {
        ReactComponent.DOMIDOperations.updatePropertyByID(
          this._rootNodeID,
          propKey,
          nextProp
        );
      }
    }
  },

这里面方法没有太多的hack技巧,非常的简单直白,不单独拧出来说,我直接写到注释里面了。

最后直接更新组件的属性

  setValueForProperty: function(node, name, value) {
    if (DOMProperty.isStandardName[name]) {
      var mutationMethod = DOMProperty.getMutationMethod[name];
      if (mutationMethod) {
        mutationMethod(node, value);
      } else if (DOMProperty.mustUseAttribute[name]) {
        if (DOMProperty.hasBooleanValue[name] && !value) {
          node.removeAttribute(DOMProperty.getAttributeName[name]);
        } else {
          node.setAttribute(DOMProperty.getAttributeName[name], value);
        }
      } else {
        var propName = DOMProperty.getPropertyName[name];
        if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) {
          node[propName] = value;
        }
      }
    } else if (DOMProperty.isCustomAttribute(name)) {
      node.setAttribute(name, value);
    }
  }

整体属性更新的流程图大概如下:

IMAGE

结束语

通篇读完,是不是有种

img

react源码中包含很多的点的知识,比如我们之前说的VDOM、包括后面要去学习dom-diff、事务、缓存等等,都是一个点,而但从一个点来切入难免有的会有些枯燥没卵用,别急别急~

img
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,360评论 1 33
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,766评论 1 18
  • 说在前面 关于 react 的总结过去半年就一直碎碎念着要搞起来,各(wo)种(tai)原(lan)因(le)。心...
    陈嘻嘻啊阅读 6,794评论 7 41
  • 前言 这篇文章主要是为了纪录一些自己对于setState的认识的不断深入的过程。我觉得这过程对我自己来说很有价值,...
    Srtian阅读 5,970评论 9 17
  • 我的英文很差,一直也不太想学,一年也用不了几次,如果学的话要花费很多时间,所以到现在还是很差。今天来了个老外,大部...
    行进中的路人甲阅读 146评论 0 0