Consistently use _children & combine Fragment and Component diffing (-73 B)#1658
Conversation
To fix hydration tests
Since `diffChildren` is always called after `diff`, `diffChildren` is the only place where VNodes are matched against oldVNodes
Note: this introduces a breaking change where component refs aren't re-invoked every render
This code had 0 code coverage. It appears that toChildArray flattens all nested arrays before they go to coerceToVNode as arrays...
55b3de1 to
d715b45
Compare
| options: { | ||
| comments: false, | ||
| compact: true, | ||
| // comments: false, |
There was a problem hiding this comment.
Note to self: Undo before checking in
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I accidentally left this in (whoops). If this causes problems, we can undo it.
| "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", |
There was a problem hiding this comment.
Note to self: Undo before checking in
There was a problem hiding this comment.
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
There was a problem hiding this comment.
oh no 👀
I think it was for direct usage with unpkg
| return createVNode(null, possibleVNode, null, null); | ||
| } | ||
|
|
||
| if (Array.isArray(possibleVNode)) { |
There was a problem hiding this comment.
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.
| ref.resetHistory(); | ||
| render(<Foo ref={ref} />, scratch); | ||
| expect(ref).to.have.been.calledOnce; | ||
| expect(ref).not.to.have.been.called; |
There was a problem hiding this comment.
These test modifications demonstrate the breaking change
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`).
_children & combine Fragment and Component diff'ing (-57 B)_children & combine Fragment and Component diff'ing (-70 B)
|
This is more a question but does this solve the issues added in: #1639 |
| * 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 |
There was a problem hiding this comment.
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.
|
Finally had a chance to read through the changes in depth. Merging Removing the special The 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 👍 👍 |
_children & combine Fragment and Component diff'ing (-70 B)_children & combine Fragment and Component diffing (-57 B)
|
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
left a comment
There was a problem hiding this comment.
This looks awesome Andre, it's a real pleasure to read through these changes! BIG BIG props to you 👍
| 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); |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
This PR is so good!! 👍 👍
|
The maxDuration line in compat/suspense seems to have lost its hit |
|
@JoviDeCroock I'm running into this even on /cc @sventschui |
|
@marvinhagemeister maybe the coverall settings reset again and it's showing branch coverage or because of an update in istanbul maybe :/ |
|
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 ✌️ |
|
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? |
_children & combine Fragment and Component diffing (-57 B)_children & combine Fragment and Component diffing (-73 B)
- 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.
- 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.
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.component._prevVNodewithvnode._childrenDetails
Replace
component._prevVNodewithvnode._childrenPreviously,
_childrenwas a cache for a normalizedprops.children. With a DOM VNode and Fragment VNode, you could access the children of the VNodes on the this_childrenproperty, but on VNodes for components, you need to go throughvnode._component._preVNode.This PR formalizes the concept behind
_children. It is now the container for the rendered children of all VNodes. If you think ofprops.childrenas input to render functions,_childrenis the output of therender. And DOM VNodes and Fragments are just render functions that returnprops.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
diffwas called with a Component, it would callc.render, coerce the result to a single VNode (e.g. convert arrays to a Fragment), and then calldiff. However, building on the_childrenchange (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,diffalways callsdiffChildrenafter 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->diffChildrenfor all VNodes allows us to do operations that require knowing theparentVNodeindiffChildrenwithout having to special case thediff->diffsituation. For example, there is now only one place where oldVNode matching happens:diffChildren(we can remove some code from the top ofdiff). Further, mounting and unmounting DOM only happens indiffChildrenas well (if unmounting wasn't recursive, we could inline it).Future Considerations
With this PR, their are only 2 place
diffis called:diffChildrenandforceUpdate. If we can changeforceUpdateto also enter the diff throughdiffChildren, then it is conceivable that we could inlinediffintodiffChildrenand 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 5try {} 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.
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
diffChildrenafter rendering components (-8 B)b09318f Copy the oldVNode in
forceUpdateto prevent side effect modification (+7 B)dc86517 Set
vnode._dompointer 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.