1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Reactのstateはどのように推移しているのか その5

Posted at

そもそもsetStateなどでstateを変えるタイミングとは

  • 最初にこれを考えるべきでした
  • stateを変える=DOMのeventがfireするとき
  • この前提を無視してはいつまでも内部の構造がわからないはずです

今回考えるケース

case.js
import React from 'react'
import { render } from 'react-dom'

class Case1 extends React.Component {
  constructor(props) {
    super(props)
    this.state = { cost: 1000 }
    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(e) {
    this.setState({ cost: e.target.value })
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.cost}
          onChange={this.handleChange}
        />
      </div>
    )
  }
}

render(
  <Case1 />,
  document.getElementById('exmaple')
)

典型的なテキストボックスを考えます。
それではReactの内部を見ていきますが、この場合はReactDOMComponent.mountComponentが実行されるのがわかっているので、そこから見ていきます。
なお、<div>の処理は無視しても大丈夫です。
同じ処理が<div><input>で実行されるだけです。

ReactDOMComponent.js
  mountComponent: function(
    transaction,
    nativeParent,
    nativeContainerInfo,
    context
  ) {
    ...,

    switch (this._tag) {
      ...,

      /*
       * props = {
       *   type: "text",
       *   value: 1000,
       *   onChange: this.handleChange
       * }
       */
      case 'input':
        ReactDOMInput.mountWrapper(this, props, nativeParent);
        props = ReactDOMInput.getNativeProps(this, props);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;

      ...,

      
    }
  },

まずはpropsの加工からです。

ReactDOMInput.js
var ReactDOMInput = {
  ...,

  mountWrapper: function(inst, props) {
    var defaultValue = props.defaultValue;
    inst._wrapperState = {
      initialChecked: props.defaultChecked || false,
      initialValue: defaultValue != null ? defaultValue : null,
      listeners: null,
      onChange: _handleChange.bind(inst),
    };

    ...,
  },
};

これなんでnativeParent渡しているんだろうか?消し忘れかな?contribute chance?
_handleChangeを見てみます。

_handleChange
function _handleChange(event) {
  var props = this._currentElement.props;

  var returnValue = LinkedValueUtils.executeOnChange(props, event);

  // Here we use asap to wait until all updates have propagated, which
  // is important when using controlled components within layers:
  // https://github.com/facebook/react/issues/1698
  ReactUpdates.asap(forceUpdateIfMounted, this);

  var name = props.name;
  if (props.type === 'radio' && name != null) {
    var rootNode = ReactDOMComponentTree.getNodeFromInstance(this);
    var queryRoot = rootNode;

    while (queryRoot.parentNode) {
      queryRoot = queryRoot.parentNode;
    }

    // If `rootNode.form` was non-null, then we could try `form.elements`,
    // but that sometimes behaves strangely in IE8. We could also try using
    // `form.getElementsByName`, but that will only return direct children
    // and won't include inputs that use the HTML5 `form=` attribute. Since
    // the input might not even be in a form, let's just use the global
    // `querySelectorAll` to ensure we don't miss anything.
    var group = queryRoot.querySelectorAll(
      'input[name=' + JSON.stringify('' + name) + '][type="radio"]');

    for (var i = 0; i < group.length; i++) {
      var otherNode = group[i];
      if (otherNode === rootNode ||
          otherNode.form !== rootNode.form) {
        continue;
      }
      // This will throw if radio buttons rendered by different copies of React
      // and the same name are rendered into the same form (same as #1939).
      // That's probably okay; we don't support it just as we don't support
      // mixing React radio buttons with non-React ones.
      var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode);
      invariant(
        otherInstance,
        'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
        'same `name` is not supported.'
      );
      // If this is a controlled radio button group, forcing the input that
      // was previously checked to update will cause it to be come re-checked
      // as appropriate.
      ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
    }
  }

  return returnValue;
}

instをthisにbindしてるので、inputがこのmethodをonChangeイベント時に実行します。
LinkedValueUtilsを見てみます。

LinkedValueUtils.js
var LinkedValueUtils = {
  ...,

  executeOnChange: function(inputProps, event) {
    if (inputProps.valueLink) {
      _assertValueLink(inputProps);
      return inputProps.valueLink.requestChange(event.target.value);
    } else if (inputProps.checkedLink) {
      _assertCheckedLink(inputProps);
      return inputProps.checkedLink.requestChange(event.target.checked);
    } else if (inputProps.onChange) {
      return inputProps.onChange.call(undefined, event);
    }
  },
};

ここで自分で定義したonChangeハンドラが実行されるようです。
戻ります。ReactUpdates.asapを見てみます。

ReactUpdates.js
var asapCallbackQueue = CallbackQueue.getPooled();
var asapEnqueued = false;

function asap(callback, context) {
  ...,

  asapCallbackQueue.enqueue(callback, context);
  asapEnqueued = true;
}

enqueueにより、CallbackQueue.callbackとCallbackQueue.contextにそれぞれ追加されます。
それでは、ReactDOMInputに戻ると、次にgetNativePropsが実行されています。

ReactDOMInput.js
var ReactDOMInput = {
  getNativeProps: function(inst, props) {
    var value = LinkedValueUtils.getValue(props);
    var checked = LinkedValueUtils.getChecked(props);

    var nativeProps = assign({
      // Make sure we set .type before any other properties (setting .value
      // before .type means .value is lost in IE11 and below)
      type: undefined,
    }, props, {
      defaultChecked: undefined,
      defaultValue: undefined,
      value: value != null ? value : inst._wrapperState.initialValue,
      checked: checked != null ? checked : inst._wrapperState.initialChecked,
      onChange: inst._wrapperState.onChange,
    });

    return nativeProps;
  },

  ...,
};

再度、LinkedValueUtilsを見てみます。

LinkedValueUtils.js
var LinkedValueUtils = {
  ...,

  getValue: function(inputProps) {
    if (inputProps.valueLink) {
      _assertValueLink(inputProps);
      return inputProps.valueLink.value;
    }
    return inputProps.value;
  },

  getChecked: function(inputProps) {
    if (inputProps.checkedLink) {
      _assertCheckedLink(inputProps);
      return inputProps.checkedLink.value;
    }
    return inputProps.checked;
  },

  ...,
};

valueLink, checkedLinkがあればそれを返して、なければpropsのvalueとcheckedを返しています。
getNativePropsに戻ると、nativePropsを作成しています。
ここで注意なのが、assignは重複したpropertyを持っている場合、後にassignされたものが優先されるということです。
そのため、props.onChangeがwrapperのに打ち消されています。
ただ、wrapperには元の処理がwrapされているので問題はないです。
ReactDOMComponent.mountComponentに戻って、最後にqueueにcallbackを突っ込んでいます。
あとあと見ることになると思うので、ここでは無視します。

ReactDOMComponent.mountComponent
続き

      ...,

      el = ownerDocument.createElement(this._currentElement.type);

      ...,

ownerDocument=documentなので新しいinputのDOMを作っているだけです。

ReactDOMComponent.mountComponent
続き

      ...,

      this._updateDOMProperties(null, props, transaction);
      var lazyTree = DOMLazyTree(el);
      this._createInitialChildren(transaction, props, context, lazyTree);
      mountImage = lazyTree;

      ...,

次に_updateDOMPropertiesを見ていきます。
その4で実行される箇所はわかっているので、そこをピンポイントで見ていきます。

_updateDOMProperites
      ...,

      } else if (registrationNameModules.hasOwnProperty(propKey)) {
        if (nextProp) {
          enqueuePutListener(this, propKey, nextProp, transaction);
        } else if (lastProp) {
          deleteListener(this, propKey);
        }
      }

      ...,

propKey=onChnage, nextProp=onChangeのhandlerです。
やっとqueueにListenerを設定している箇所までたどり着きました。
見ていきます。
が、長くなったので次回。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?