Skip to content

Consistently use _children & combine Fragment and Component diffing (-73 B)#1658

Merged
andrewiggins merged 38 commits into
masterfrom
vnode-prop-explorations
Jun 5, 2019
Merged

Consistently use _children & combine Fragment and Component diffing (-73 B)#1658
andrewiggins merged 38 commits into
masterfrom
vnode-prop-explorations

Conversation

@andrewiggins

@andrewiggins andrewiggins commented May 29, 2019

Copy link
Copy Markdown
Member

Summary

NOTE: this might be a good candidate for merging after we release 10.0. Given the size of the change, it may not be worth de-stabilizing master with more changes. Thoughts? I might need this change to improve #1605, so perhaps it would be worthwhile to get in soon. Still investigating.

  • Replace component._prevVNode with vnode._children
  • Combine Fragment and Component diffing

Details

Replace component._prevVNode with vnode._children

Previously, _children was a cache for a normalized props.children. With a DOM VNode and Fragment VNode, you could access the children of the VNodes on the this _children property, but on VNodes for components, you need to go through vnode._component._preVNode.

This PR formalizes the concept behind _children. It is now the container for the rendered children of all VNodes. If you think of props.children as input to render functions, _children is the output of the render. And DOM VNodes and Fragments are just render functions that return props.children. The implementation is a little different (DOM VNodes don't have a render function, per se), but this concept applies.

Combine Fragment and Component diffing

Previously, if diff was called with a Component, it would call c.render, coerce the result to a single VNode (e.g. convert arrays to a Fragment), and then call diff. However, building on the _children change (and the concept mentioned there), we now instead treat Components as always returning arrays, and coerce result of render into a children of VNodes to continue rendering. So, diff always calls diffChildren after diff'ing a VNode, regardless of its type. Another way to think about this is that everything is Fragment now 😄

Being able to depend on diffChildren -> diff -> diffChildren for all VNodes allows us to do operations that require knowing the parentVNode in diffChildren without having to special case the diff -> diff situation. For example, there is now only one place where oldVNode matching happens: diffChildren (we can remove some code from the top of diff). Further, mounting and unmounting DOM only happens in diffChildren as well (if unmounting wasn't recursive, we could inline it).

Future Considerations

With this PR, their are only 2 place diff is called: diffChildren and forceUpdate. If we can change forceUpdate to also enter the diff through diffChildren, then it is conceivable that we could inline diff into diffChildren and make our diff a single function! I think this could give us some good byte savings by sharing more local variables and reducing duplicate structures (e.g. have fewer than 5 try {} catch {} like we currently do). It may reduce the approachability of our code, but perhaps we can mitigate that with a well-defined sectional comment format to make up for the lost function names. Either way, I think it might be worth an exploration.

Breaking change

There are some breaking changes in this PR.

  • Component refs aren't re-invoked after each render. Preact v8 and v10 both re-invoke component refs on every render. This PR changes that behavior so they are only invoked on initial render, when the ref changes, or on unmount. This new behavior appears to match React.

Per change summary

91f043b Only copy _dom from oldVNode to newVNode if necessary (-1 B)
3159b71 Replace c._prevVNode with vnode._children (-2 B)
b73ab00 Call diffChildren after rendering components (-8 B)
b09318f Copy the oldVNode in forceUpdate to prevent side effect modification (+7 B)
dc86517 Set vnode._dom pointer in diffChildren (+2 B)
fe7f20d Only remove excessDomChildren after diff'ing DOM VNodes (+5 B)
6f06653 Remove redundant vnode type checks in diff (-17 B)
40212eb Apply refs after unmounting in diffChildren (+25 B)
910f98c Combine Fragment and Component diffing (-60 B)
27116bc Remove Array coercion from coerceToVNode (-8 B)
6b18f6f Remove more null checks from diff (-13 B)
c0c070e Normalize oldVNode to EMPTY_OBJ once (-2 B)
9d89d10 Fix Fragment _dom pointers (-10 B)
3470318 Flatten top-level Fragments returned from render (+21 B)
31036a3 Remove unused append in forceUpdate (-14 B)

Total change: -70 B 😁
Total change: -78 B 😁
Total change: -57 B 😃
Total change: -73 B 😁

NOTE: Individual commit size diffs may not be exact due to drifting caused by merges from master. They are intended to give an estimate of relative cost.

@andrewiggins andrewiggins force-pushed the vnode-prop-explorations branch from 55b3de1 to d715b45 Compare May 29, 2019 19:00
Comment thread karma.conf.js
options: {
comments: false,
compact: true,
// comments: false,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note to self: Undo before checking in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm wondering if we should remove these options from our karma config. Last time I checked they were breaking sourcemaps and thus any breakpoints.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I accidentally left this in (whoops). If this causes problems, we can undo it.

Comment thread package.json
"build:test-utils": "microbundle build --raw --cwd test-utils",
"build:compat": "microbundle build --raw --cwd compat --globals 'preact/hooks=preactHooks'",
"dev": "microbundle watch --raw --format cjs,umd",
"dev": "microbundle watch --raw --format cjs",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note to self: Undo before checking in

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Whoops, forgot this one too 🤦‍♂. When comparing sizes I usually just look at cjs (for no real reason, it's just first in the list), so I remove the umd cuz it just slows down the build. If there is a reason to keep both, happy to undo and change my habits lol

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

oh no 👀

I think it was for direct usage with unpkg

Comment thread src/create-element.js
return createVNode(null, possibleVNode, null, null);
}

if (Array.isArray(possibleVNode)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This change should be okay since toChildArray is always flattens nested arrays before coerceToVNode is called, but it does make me a little nervous. We don't have any tests that used these lines so I think they are safe to remove.

Comment thread test/browser/refs.test.js
ref.resetHistory();
render(<Foo ref={ref} />, scratch);
expect(ref).to.have.been.calledOnce;
expect(ref).not.to.have.been.called;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These test modifications demonstrate the breaking change

Comment thread test/browser/fragments.test.js Outdated
The only place `diff` is called not inside `diffChildren` (which already does proper null checking) is `forceUpdate`. `forceUpdate` will never pass in a null newVNode or oldVNode cuz it always passes the VNode that represents the component. If the component is in the tree, it can't have a null VNode (or else it doesn't exist in the tree. It can render null, but that is handled by `diffChildren`).
@andrewiggins andrewiggins changed the title Consistently use _children & combine Fragment and Component diff'ing (-57 B) Consistently use _children & combine Fragment and Component diff'ing (-70 B) May 29, 2019
@JoviDeCroock

Copy link
Copy Markdown
Member

This is more a question but does this solve the issues added in: #1639

Comment thread src/diff/index.js
* Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
*/
export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom) {
// If the previous type doesn't match the new type we drop the whole subtree

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These checks are not necessary when diff is always called by diffChildren (there is one exception, forceUpdate, discussed below). diffChildren already does proper null checking on newVNode and passes in EMPTY_OBJ if oldVNode is null. diffChildren will never pass two vnodes of non-equal types. It will instead always pass null if it can't find a matching oldVNode for newVNode.

The only place diff is called not inside diffChildren is forceUpdate. However, forceUpdate always calls diff with non-null vnodes of the same type. It will never pass in a null newVNode or oldVNode cuz it always passes the VNode that represents the component. If the component is in the tree, it can't have a null VNode or else it doesn't exist in the tree. It can render null, but that is handled by diffChildren.

In other words, all of this logic is duplicated in diffChildren. Since we now call always diffChildren after diff, we no longer need to duplicate that logic here.

Comment thread src/diff/children.js Outdated
@marvinhagemeister

Copy link
Copy Markdown
Member

Finally had a chance to read through the changes in depth. Merging _prevVNode and _children is such a relief. It was causing me a lot of trouble with the devtools and always using _children makes it so much simpler 🎉

Removing the special diff -> diff case in particular is very exciting! Making every component a Fragment is a more sensible path forward and should help us normalize more things 👍 I'll need to have a closer look again after JSConf is over, but I'm hoping that it will help us find a better solution to the sibling issue #1605 .

The forceUpdate stuff can be postponed to another PR in my opinion. It may take a bit of thought to get it right too. Although I'm confident that this PR will make it a lot easier. I think you have a really good handle on that :)

Regarding refs: Calling them on every render seems unnecessary. I'm with you that just calling them when something changes is sufficient 💯

Overall this PR is (like your others) top notch and the creme de la creme. It's just that good 👍 👍

@andrewiggins andrewiggins changed the title Consistently use _children & combine Fragment and Component diff'ing (-70 B) Consistently use _children & combine Fragment and Component diffing (-57 B) Jun 1, 2019
@andrewiggins andrewiggins marked this pull request as ready for review June 1, 2019 07:09
@coveralls

coveralls commented Jun 1, 2019

Copy link
Copy Markdown

Coverage Status

Coverage decreased (-0.002%) to 99.775% when pulling a665861 on vnode-prop-explorations into 896e77d on master.

@robertknight

Copy link
Copy Markdown
Member

Nice work! I'm not as deeply familiar with the diffing logic as other devs but conceptually it looks like a good simplification. Assuming this lands before the next beta, I will need to update the Enzyme adapter accordingly, as it accesses the rendered children of vnodes.

@JoviDeCroock JoviDeCroock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks awesome Andre, it's a real pleasure to read through these changes! BIG BIG props to you 👍

Comment thread src/component.js
let mounts = [];
dom = diff(parentDom, vnode, vnode, this._context, parentDom.ownerSVGElement!==undefined, null, mounts, this._ancestorComponent, force, dom);
if (dom!=null && dom.parentNode!==parentDom) {
parentDom.appendChild(dom);

@andrewiggins andrewiggins Jun 3, 2019

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed the appendChild call here because code coverage was complaining (showing this line was never hit), which I take to mean that either it is no longer needed, or that we are missing a test case to justify it's purpose. However, since #1605 is going to completely rewrite this code anyway, I'll just remove this now to keep coveralls happy.

@marvinhagemeister marvinhagemeister left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR is so good!! 👍 👍

@JoviDeCroock

Copy link
Copy Markdown
Member

The maxDuration line in compat/suspense seems to have lost its hit

@marvinhagemeister

Copy link
Copy Markdown
Member

@JoviDeCroock I'm running into this even on master. Not sure how it got in :S

/cc @sventschui

@JoviDeCroock

Copy link
Copy Markdown
Member

@marvinhagemeister maybe the coverall settings reset again and it's showing branch coverage or because of an update in istanbul maybe :/

@sventschui

Copy link
Copy Markdown
Member

I guess maxDuration was never tested. I don‘t know how to test this in mocha. I know jest supports mocking timers :/

I‘m in a university seminar this week. Can look into this next week ✌️

@andrewiggins

Copy link
Copy Markdown
Member Author

I think coveralls is failing cuz I changed the number of lines in suspense.js, so the one line that wasn't covered caused a tiny percentage change cuz it is now a greater percentage of lines (since the total number of lines is smaller).

I think we can proceed without coveralls approval since it doesn't actually represent a change in functional coverage.

Are people okay if I merge this?

@andrewiggins andrewiggins changed the title Consistently use _children & combine Fragment and Component diffing (-57 B) Consistently use _children & combine Fragment and Component diffing (-73 B) Jun 5, 2019
@andrewiggins andrewiggins merged commit b8c0e87 into master Jun 5, 2019
@andrewiggins andrewiggins deleted the vnode-prop-explorations branch June 5, 2019 22:57
robertknight added a commit to preactjs/enzyme-adapter-preact-pure that referenced this pull request Jun 10, 2019
 - The `component._prevVNode` VNode property containing the rendered output
   of a component was replaced by lookups through
   `component._vnode._children`, which is always an array.

 - A `Fragment` is now simply a regular function component which returns
   `props.children`, so we need to test for whether the node is a
   fragment before testing whether it is a custom component

 - Handle the possibility of `vnode.props` being `null`. It doesn't
   appear that is actually possible from the code, but the current
   Preact 10 upstream typings specify that props may be an object,
   a string/number (this definitely can happen) or null

See preactjs/preact#1658 for additional details.
robertknight added a commit to preactjs/enzyme-adapter-preact-pure that referenced this pull request Jun 10, 2019
 - The `component._prevVNode` VNode property containing the rendered output
   of a component was replaced by lookups through
   `component._vnode._children`, which is always an array.

 - A `Fragment` is now simply a regular function component which returns
   `props.children`, so we need to test for whether the node is a
   fragment before testing whether it is a custom component

 - Handle the possibility of `vnode.props` being `null`. It doesn't
   appear that is actually possible from the code, but the current
   Preact 10 upstream typings specify that props may be an object,
   a string/number (this definitely can happen) or null

See preactjs/preact#1658 for additional details.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants