Possible code duplication in blink::HTMLElement::offset*ForBinding and blink::Element::offset* ? |
|||||
Issue description
Currently, blink::HTMLElement and blink::Element both implement methods for calculating element offsets. These are named offset{Top/Left/Height/Width}ForBinding in HTMLElement, and offset{Top/Left/Height/Width} in Element. Note that HTMLElement inherits from Element, so it also has access to the non-suffixed methods.
To me, it appears that the implementation of these methods are *nearly* functionally equivalent, with two exceptions:
1. kochi@'s fix for layout object lifecycle (https://crrev.com/5c480f3117fe314b8cace4f33b84020e9e3424e2) was landed only for HTMLElement and not Element.
2. They have slightly different behavior in LayoutObject::offsetParent, due to passing 'this' versus a null pointer. See below for details on this.
So who calls each version of these methods? As the name may suggest, the offset*ForBinding methods are bindings for the V8 HTMLElement interface, so are the ones called when Javascript calls the offset* methods (e.g. https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop). The Element offset* methods are called in very few places: blink::ImageDocument::imageClicked and blink::SpatialNavigation::rectToAbsoluteCoordinates (static method) are the only non-test locations.
This entire thing smells of code duplication to me, and I'm hoping that we can combine them such that HTMLElement::offset*ForBinding just call their related Element::offset* methods. We would need to clear up #1 and #2 above first though.
To give a concrete walkthrough, let us compare offsetTopForBinding and offsetTop:
int HTMLElement::offsetTopForBinding() {
Element* offsetParent = unclosedOffsetParent();
if (LayoutBoxModelObject* layoutObject = layoutBoxModelObject())
return adjustLayoutUnitForAbsoluteZoom(
LayoutUnit(layoutObject->pixelSnappedOffsetTop(offsetParent)),
layoutObject->styleRef())
.round();
return 0;
}
int Element::offsetTop() {
document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);
if (LayoutBoxModelObject* layoutObject = layoutBoxModelObject())
return adjustLayoutUnitForAbsoluteZoom(
LayoutUnit(layoutObject->pixelSnappedOffsetTop(offsetParent())),
layoutObject->styleRef())
.round();
return 0;
}
Apart from when the offset parent is calculated (as noted, changed by kochi@ in HTMLElement to fix lifecycle issues), the only differences are that Element::offsetTop() calls 'document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);' at the start of its method, and they have differently named methods for getting the offset parent.
Let us look at the offset parent methods:
Element* HTMLElement::unclosedOffsetParent() {
document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);
LayoutObject* layoutObject = this->layoutObject();
if (!layoutObject)
return nullptr;
return layoutObject->offsetParent(this);
}
Element* Element::offsetParent() {
document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this);
LayoutObject* layoutObject = this->layoutObject();
return layoutObject ? layoutObject->offsetParent() : nullptr;
}
Note that both of these call 'document().updateStyleAndLayoutIgnorePendingStylesheetsForNode(this)', so that difference between the parent offset* methods is gone. However, the call to LayoutObject::offsetParent is slightly different - HTMLElement passes 'this', whilst Element passes nullptr (default parameter value). So what impact does that have? In LayoutObject::offsetParent:
// TODO(kochi): If |base| or |node| is nested deep in shadow roots, this
// loop may get expensive, as isUnclosedNodeOf() can take up to O(N+M) time
// (N and M are depths).
if (base &&
(node->isClosedShadowHiddenFrom(*base) ||
(node->isInShadowTree() &&
node->containingShadowRoot()->type() == ShadowRootType::UserAgent))) {
// If 'position: fixed' node is found while traversing up, terminate the
// loop and return null.
if (ancestor->isFixedPositioned())
return nullptr;
continue;
}
This is the only difference - base will be the element for HTMLElement's call, and nullptr for Element's call. Sadly, I'm not familiar enough with blink to understand the impact of this shadow tree checking.
,
Jan 25 2017
Thanks for taking a close look at the code. The need for the offset* change was due to discussion on https://github.com/w3c/csswg-drafts/issues/159 , TL;DR if a offsetParent hits inside Shadow DOM, such offsetParent should be adjusted to appropriate element. And the issue was re-opened for further change, but we haven't got to a conclusion. For more detailed background for the change, please check https://codereview.chromium.org/2051703002 rather than LayoutObject refcount bugfix you quoted. The offset*ForBinding APIs are split from the original offset* because of the ShadowDOM handling. There are still some users of the original one, and in order not to break those, the non-binding interfaces are left there. Probably you can take a close look at their usage and can be converted to use *ForBinding variant, and definitely there are clean up opportunities around offset* code in both dom and layout. I'm happy to review CLs for this.
,
Jan 25 2017
,
Jan 25 2017
Thanks Kochi for the quick response and excellent overview. I see from the original review that hayato@ was the one who had concerns about the existing callers of Element::offset*, and requested that the new offset*ForBinding methods be created instead. hayato: Before I dig into the call sites myself (blink::ImageDocument::imageClicked and rectToAbsoluteCoordinates in blink::SpatialNavigation) - did you have specific knowledge on how these would break if Element::offset* was changed to respect unclosed shadow DOM parents, or were you just concerned that we didn't know the effect?
,
Jan 26 2017
Yeah, at that time we didn't have much time to investigate each call site whether it works with *ForBinding, so we left all the non-binding callers as is. Please note that CSSOM spec may have further change depending on the conclusion of https://github.com/w3c/csswg-drafts/issues/159 , so it may be premature to convert all the offset* sites to *ForBinding, so I'd say keeping both interfaces for now is a safer option, although I believe even while we keep both interfaces we can clean code up better.
,
Mar 16 2017
,
Feb 27 2018
|
|||||
►
Sign in to add a comment |
|||||
Comment 1 by smcgruer@chromium.org
, Jan 24 201713.8 MB
13.8 MB Download