Skip to content
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

Refactor DOM attribute code #11804

Merged
merged 17 commits into from
Dec 8, 2017
Merged

Refactor DOM attribute code #11804

merged 17 commits into from
Dec 8, 2017

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Dec 8, 2017

The main goal here is to factor the DOM attribute code in the light of how it currently works, instead of how it was conceived five years ago.

There are quite a few things in the current code that don't make sense anymore. For example, the "property" vs "attribute" distinction in method naming doesn't actually correspond to properties vs attributes. Another example: shouldSetAttribute doesn't actually determine whether to set an attribute. These were all results of incremental changes where we tried to tweak the behavior with the least amount of code. But they ended up making the overall flow confusing and adding a lot of special cases.

This is an attempt to simplify the code without changing what it does. Individual commits tell how I arrived at that. This mostly changes the logic around setting attributes/properties and doesn't touch the other parts that are still structured the old way (such as get*Attribute|Property pairs used for validating SSR). I think we can get to those a bit later.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 8, 2017

Lol, initial bench results were seriously skewed because my screensaver turned on 😄

It seems like this is a little bit slower but with huge variance. So hard to say anything conclusive.

screen shot 2017-12-08 at 00 40 20

screen shot 2017-12-08 at 00 40 13

}
// The rest are treated as attributes with special cases.
const {attributeName, attributeNamespace} = propertyInfo;
if (value === null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can value be undefined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, this would get caught by:

  if (shouldTreatAttributeValueAsNull(name, value, isCustomComponentTag)) {
    value = null;
  }

}

export function shouldTreatAttributeValueAsNull(name, value) {
if (value === null || typeof value === 'undefined') {
Copy link
Contributor

@trueadm trueadm Dec 8, 2017

Choose a reason for hiding this comment

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

why do we need to do typeof value === 'undefined'? why not just value === undefined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Consistency with all other checks. (I don't care either way but IMO we should change them all together if we want to.)

return prefix !== 'data-' && prefix !== 'aria-';
}

export function shouldTreatAttributeValueAsNull(name, value) {
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 add type annotations to this function?

@@ -80,30 +68,29 @@ export function createMarkupForRoot() {
* @return {?string} Markup string, or null if the property was invalid.
*/
export function createMarkupForProperty(name, value) {
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 also added type annotations here whilst we're at it?

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

Overall, it looks good – the slight performance regression isn't a concern to me. Please, as this is a refactor PR, can we add type annotations to the functions used in this process. I think it will add to the readability and help reduce further bugs if we can. Otherwise, awesome stuff!

@@ -152,84 +127,62 @@ export function getValueForAttribute(node, name, expected) {
* @param {string} name
* @param {*} value
*/
export function setValueForProperty(node, name, value) {
const propertyInfo = getPropertyInfo(name);
export function setValueForProperty(node, name, value, isCustomComponentTag) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Type annotations here too please?

}
if (value === null) {
return '';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you just return '' inside of the shouldTreatAttributeValueAsNull block? When does shouldTreatAttributeValueAsNull return false but the value is null?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch

return false;
}

export function isBadlyTypedAttributeValue(name, value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

if (isBadlyTypedAttributeValue(name, value)) {
return true;
}
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just return isBadlyTypedAttributeValue(name, value)?

I noticed some patterns weren't being tested.
The branching before the call is unnecessary because setValueForProperty() already
has an internal branch that delegates to deleteValueForProperty() for null and
undefined through the shouldIgnoreValue() check.

The goal is to start unifying these methods because their separation doesn't
reflect the current behavior (e.g. for unknown properties) anymore, and obscures
what actually happens with different inputs.
Now we don't read propertyInfo twice in this case.

I also dropped a few early returns. I added them a while ago when we had
Stack-only tracking of DOM operations, and some operations were being
counted twice because of how this code is structured. This isn't a problem
anymore (both because we don't track operations, and because I've just
inlined this method call).
The special cases for null and undefined already exist in setValueForAttribute().
Their naming is pretty confusing by now. For example setValueForProperty()
calls setValueForAttribute() when shouldSetAttribute() is false (!). I want
to refactor (as in, inline and then maybe factor it out differently) the relation
between them. For now, I'm consolidating the callers to use setValueForProperty().
The naming of these methods is still very vague and conflicting in some cases.
Will need further work.
@gaearon gaearon force-pushed the dom-injection-2 branch 2 times, most recently from 482a300 to 908a807 Compare December 8, 2017 18:37
This makes the flow clearer in my opinion.
It was previously duplicated.

It's also suspiciously similar in purpose to shouldTreatAttributeValueAsNull()
so I want to see if there is a way to unify them.
Its naming was confusing and it was used all over the place instead of more specific checks.
Now that we only have one call site, we might as well inline and get rid of it.
// If the prop is in the special list, treat it as a simple attribute.
if (!propertyInfo) {
if (isAttributeNameSafe(name)) {
const attributeName = name;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why alias name to attributeName?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To make it extra clear you're not supposed to call setAttribute with name unless you know what you're doing.

* @param {DOMElement} node
* @param {string} name
*/
export function deleteValueForProperty(node, name) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you rename this test since deleteValueForProperty doesn't exist anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we care? There's plenty of internal names, even in test files (e.g. ReactDOMComponent-test). I agree we need to clean it up by functionality rather than file, but don't think it's necessary to do in the scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Using names of internal APIs that exist is different. In this case, it's referrencing a function that no longer exists. If I was looking to contribute and encountered that, I would be confused that the function didn't exist anywhere in the codebase.

It's not critical, but since you remove it feels like it's in scope to fix any lingering references. If we're going to restructure the tests soon anyways, it probably doesn't matter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess what I'm referring to is that there are already plenty examples where internal names don't match tests. It used to be important for them to be in sync when unit test tested internals, but they don't anymore. I agree it's confusing, but IMO mismatches in test file names are even more confusing.

Conceptually those tests are about deleting. I wouldn't move them into set*ForProperty just because those code paths moved there. Instead I would've preferred to rename all tests to human readable descriptions (e.g. "deletion"). But then it's already inconsistent in the current test codebase so I think it's easier to do in one big cleanup later.

if (shouldTreatAttributeValueAsNull(name, value, isCustomComponentTag)) {
value = null;
}
// If the prop is in the special list, treat it as a simple attribute.
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean if the prop is not in the special list?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops yes

if (mustUseProperty) {
const {propertyName} = propertyInfo;
if (value === null) {
(node: any)[propertyName] = hasBooleanValue ? false : '';
Copy link
Contributor

@aweary aweary Dec 8, 2017

Choose a reason for hiding this comment

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

All attributes that must use property are boolean values, so technically this check is unnecessary. We could add a new attribute type like HAS_BOOLEAN_PROPERTY to simplify this, and get rid of the bitmasks again (happy to do in a follow up)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's follow up. This diff is getting hard to deal with.

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

Thanks for adding Flow type annotations. LGTM (minus lint error I think?) :)

@gaearon gaearon merged commit 47783e8 into facebook:master Dec 8, 2017
@gaearon gaearon deleted the dom-injection-2 branch December 8, 2017 20:42
@gaearon
Copy link
Collaborator Author

gaearon commented Dec 8, 2017

Ran benchmarks a few times, seems neutral.

gaearon added a commit that referenced this pull request Dec 8, 2017
gaearon added a commit that referenced this pull request Dec 8, 2017
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.

None yet

5 participants