Skip to content

Eliminate unnecessary numeric equality checks #11751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

nhunzaker
Copy link
Contributor

@nhunzaker nhunzaker commented Dec 2, 2017

This commit changes the way numeric equality for number inputs works such that it compares loose equality for numbers. This seems sufficient, and eliminates quite a bit of branching around numeric equality.

This additionally addresses Flow type issues on #11741, and it is based off that branch

Test plan:

  1. Open http://react-concise-numbers.surge.sh
  2. Run through the Text and Number test fixtures

Tested in:

  • Chrome 43, 62
  • Safari 7.1, 11
  • Firefox 52 (ESR), 58
  • IE 9, 10, 11
  • Edge 14, 16

This commit changes the way numeric equality for number inputs works
such that it compares against `input.valueAsNumber`. This eliminates
quite a bit of branching around numeric equality.
@gaearon
Copy link
Collaborator

gaearon commented Dec 2, 2017

Can you explain why the original code was written this way, and why it's no longer necessary despite comments saying something about IE9?

) {
if (props.type === 'number') {
// eslint-disable-next-line
if (node.valueAsNumber !== value && node.value != value) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I wonder if I even need to check valueAsNumber. Everything works as expected in IE9, which does not support this API.

) {
if (props.type === 'number') {
// eslint-disable-next-line
if (node.value != value) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be too good to be true, but this checks out in every browser. Unit tests also confirm it. Have I missed something?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking git blame to see why this was introduced.

) {
if (props.type === 'number') {
// eslint-disable-next-line
if (node.value != value || (value === 0 && node.value === '')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we didn't cover "" to 0 in our unit tests. Added.

@nhunzaker
Copy link
Contributor Author

Can you explain why the original code was written this way [...]

Identifying all of the edge cases was a long road, and we didn't have all of the test fixtures and unit tests to cover edge cases in type coercion. It was the result of adding additional branches to cover different cases.

[...] and why it's no longer necessary despite comments saying something about IE9?

IE9 does not support input.valueAsNumber, so we ran parseFloat on the value. However it looks like this is not necessary. The comment about IE9 justifies why we have to do that.

I think the gist of this is just that we do not need to parse the value as a number. We can make the following reductions (or at least this is my logic so we can try to poke holes in it):

if (props.type === 'number') {
     // Simulate `input.valueAsNumber`. IE9 does not support it
     var valueAsNumber = parseFloat(node.value) || 0;
     if (
       // eslint-disable-next-line
       value != valueAsNumber ||
       // eslint-disable-next-line
       (value == valueAsNumber && node.value != value)
     ) {

value == valueAsNumber after doing a check for value != valueAsNumber is extraneous. We can collapse the check into:

if (props.type === 'number') {
     // Simulate `input.valueAsNumber`. IE9 does not support it
     var valueAsNumber = parseFloat(node.value) || 0;
     if (
       // eslint-disable-next-line
       value != valueAsNumber && node.value != value
     ) {

To the best of my ability to test, == and != have the same effect on a string as parseFloat. That means we can remove it:

if (props.type === 'number') {
     // eslint-disable-next-line
     if (value != node.value && node.value != value) {

This is the same check! So finally we get to:

if (props.type === 'number') {
     // eslint-disable-next-line
     if (value != node.value) {

The edge case here is that '' loose equals 0. That is controlled for in a branch earlier in the function, but we can collapse them to the same line:

if (props.type === 'number') {
     // eslint-disable-next-line
     if (if (node.value != value || (value === 0 && node.value === '')) {

@@ -248,6 +248,23 @@ describe('ReactDOMInput', () => {
}
});

it('performs as state change from "" to 0', () => {
Copy link
Contributor

@aweary aweary Dec 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"performs a state change"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it in 2ba3fe8

(value == valueAsNumber && node.value != value)
) {
if (props.type === 'number') {
// eslint-disable-next-line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this so it's only disabling the coercion rule?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea. I also moved the loose check after the value === 0 guard to avoid some extra DOM access. 3cdde77

) {
if (props.type === 'number') {
// eslint-disable-next-line
if (node.value != value || (value === 0 && node.value === '')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this change the 0/ "" coercion would occur for any type of input. Now it only applies to number inputs. Any chance that breaks something weird?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not from my ability to test. The strict equality check covers it.

Copy link
Contributor

@aweary aweary left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does look like parseFloat and == are logically equivalent in this situation. I can't think of any situation where it fails, so assuming all the fixtures pass in the supported browsers this LGTM

@nhunzaker
Copy link
Contributor Author

I'll check this into #11741 in a bit and that one should be good to go.

@gaearon gaearon merged commit a65e25f into fix-value-assignment Dec 4, 2017
@gaearon gaearon deleted the nh-more-concise-input-numeric-equality branch December 4, 2017 13:50
gaearon pushed a commit that referenced this pull request Dec 4, 2017
…1741)

* Ensure value and defaultValue do not assign functions and symbols

* Eliminate assignProperty method from ReactDOMInput

* Restore original placement of defaultValue reservedProp

* Reduce branching. Make assignment more consistent

* Control for warnings in symbol/function tests

* Add boolean to readOnly assignments

* Tweak the tests

* Invalid value attributes should convert to an empty string

* Revert ChangeEventPlugin update. See #11746

* Format

* Replace shouldSetAttribute call with value specific type check

DOMProperty.shouldSetAttribute runs a few other checks that aren't
appropriate for determining if a value or defaultValue should be
assigned on an input. This commit replaces that call with an input
specific check.

* Remove unused import

* Eliminate unnecessary numeric equality checks (#11751)

* Eliminate unnecessary numeric equality checks

This commit changes the way numeric equality for number inputs works
such that it compares against `input.valueAsNumber`. This eliminates
quite a bit of branching around numeric equality.

* There is no need to compare valueAsNumber

* Add test cases for empty string to 0.

* Avoid implicit boolean JSX props

* Split up numeric equality test to isolate eslint disable command

* Fix typo in ReactDOMInput test

* Add todos

* Update the attribute table
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants