React原始碼分析與實現(二):狀態、屬性更新 -> setState

nealyang231發表於2018-08-28

原文連結地址: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

相關文章