fix: store DOM boundaries on effects#12215
Conversation
|
There was a problem hiding this comment.
With a template that starts with a <!> comment, the start node is null (or undefined in the static component/render tag case), and when it's time to remove nodes Svelte finds the actual start node from the first child effect.
Currently, <noscript> is also replaced with a <!> in the client-side template, but there's no corresponding child effect, so the start node is incorrect and nodes are left behind when the parent effect is destroyed.
The easy fix was to just leave <noscript> elements in the template (albeit without children), but that changed the result of the test. The alternative would be to add a new flag, but that feels like more complexity (and we're running out of room for flags — a bitmask can only hold 31)
The refactoring in #12215 didn't take HMR into account. As a result, the anchor was passed to the HMR block, which was subsequently cleaned up on destroy - but the anchor could be shared with other components and therefore needs to stay in the dom. Passing `null` instead solves the problem. Fixes #12228
The refactoring in #12215 didn't take HMR into account. As a result, the anchor was passed to the HMR block, which was subsequently cleaned up on destroy - but the anchor could be shared with other components and therefore needs to stay in the dom. Passing `null` instead solves the problem. Fixes #12228 Fixes #12230 Fixes #12233
This is an alternative to #12182 and #11690; fixes #12177 and #12198. WIP because it needs tidying up and documenting, but I'll try and summarise the changes:
Instead of
effect.dombeing a node or an array of nodes, which is written to in an unpredictable order (leading to bugs like the aforementioned, unless you do a lot of splicing which I think we're better off avoiding), block/branch effects have aneffect.nodesproperty withstart,endandanchorproperties.This property is set when the effect's template is cloned. For example in the demo app's case...
...the
<button>element is the main effect'snodes.startandnodes.end. When it's time to destroy the effect, we just remove everything fromnodes.starttonodes.end, inclusive.In a trickier case...
{#if foo} <p>does this exist or not?</p> {/if} <p>this always exists</p>...while the
ifblock effect'snodesis straightforward — the<p>is bothstartandend— the effect containing theifblock has a trickier job:During hydration we can just use the opening hydration boundary as the
start, but in themountcase we can't do that — the first node might be the<p>inside theif, or it might be theifblock's<!>anchor, depending on the value offoo. For that reason, the effect'sstartnode isnull, which means thatget_first_nodeconsultseffect.first, recursively, until we figure out which is the first node.Components and render tags are an even trickier case as in many cases they don't create an effect at all. For that reason if the first item inside an effect is one of those components/render tags, the
startisundefinedbut can be replaced withnullor a node when the item is rendered.This is all quite complicated to describe but it actually makes most implementation details much simpler. For example dynamic elements no longer need to reach up inside their parent to muck about with
parent_effect.dom, and{@html ...}tags have an easier time as well.One notable change is that in addition to relinking each block items as we move them around, we also need to relink their effects. We probably should have been doing that anyway. Currently, this is slightly inefficient but we can probably do the relinking of both items and effects at the same time, minimising the number of operations.
A nice thing about this way of doing things is that it hopefully opens the door to a more efficient approach to hydration. Currently, we have to iterate over every
NodeListin the hydrated DOM twice (once to build up thehydrate_nodesarray that becomeseffect.dom, once inside everysiblingcall), but if there's noeffect.domthen it stands to reason that we could skip the first traversal in favour of populatingeffect.nodes.endat the end of the process, insideappend. That's left as future work, however.Before submitting the PR, please make sure you do the following
feat:,fix:,chore:, ordocs:.Tests and linting
pnpm testand lint the project withpnpm lint