そもそもsetStateなどでstateを変えるタイミングとは
- 最初にこれを考えるべきでした
- stateを変える=DOMのeventがfireするとき
- この前提を無視してはいつまでも内部の構造がわからないはずです
今回考えるケース
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>
で実行されるだけです。
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の加工からです。
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
を見てみます。
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を見てみます。
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を見てみます。
var asapCallbackQueue = CallbackQueue.getPooled();
var asapEnqueued = false;
function asap(callback, context) {
...,
asapCallbackQueue.enqueue(callback, context);
asapEnqueued = true;
}
enqueueにより、CallbackQueue.callbackとCallbackQueue.contextにそれぞれ追加されます。
それでは、ReactDOMInputに戻ると、次にgetNativePropsが実行されています。
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を見てみます。
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を突っ込んでいます。
あとあと見ることになると思うので、ここでは無視します。
続き
...,
el = ownerDocument.createElement(this._currentElement.type);
...,
ownerDocument=documentなので新しいinputのDOMを作っているだけです。
続き
...,
this._updateDOMProperties(null, props, transaction);
var lazyTree = DOMLazyTree(el);
this._createInitialChildren(transaction, props, context, lazyTree);
mountImage = lazyTree;
...,
次に_updateDOMProperties
を見ていきます。
その4で実行される箇所はわかっているので、そこをピンポイントで見ていきます。
...,
} 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を設定している箇所までたどり着きました。
見ていきます。
が、長くなったので次回。