-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Description
Describe the problem
Svelte's toolbox for DOM traversal is sweet and simple, primarily composed of $.next and $.child calls to access siblings and children respectively. This method works well for small components, but can become rather unwieldy for larger components. In larger components, reactive elements are often nested, which means that to access one element, your compiled code could have 3+ lines of:
let element = $.child(fragment);
let element_1 = $.child(element);
let element_2 = $.child(element_1);This is not only repetitive, but hard to minify, as the best minification is suboptimal:
let c = a.child(a.child(a.child(b)));Describe the proposed solution
I think that an encoded DOM traversal sequence could be useful here. Essentially, the compiler would take the sequence of nodes accessed in the component fragment and serialize it as a string. Marko does something similar to this, and I think it'd fit well for Svelte. Here's a quick look at how this could work:
let sequence = $.sequence(`|>|^|`);
let root = $.template(`<h1> </h1><button> </button>`, 1);
let on_click = (_, count) => $.update(count);
export default function Component($$anchor) {
let fragment = root();
let next = sequence(fragment);
let name = 'world';
let count = $.state(0);
let h1 = next();
h1.textContent = `Hello, ${name ?? ''}`;
let button = next();
button.__click = [on_click, count];
let text = next();
$.reset(button);
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
$.append($$anchor, fragment);
}In the above example, $.sequence takes an encoded sequence for DOM traversal. The encoding uses characters to represent different operations:
>:nextSibling^:firstChild|: stop and return current node
So, for the example above, the sequence could be interpreted as:
- Get the first child (
<h1>)- Get the sibling of the h1 (
<button>)- Get the child of the button (the text)
$.sequence's definition could look something like this:
function sequence(code) {
const [...chars] = code;
const len = chars.length;
return function(fragment) {
let index = 0;
let curr = fragment?.firstChild;
function next() {
curr = curr?.nextSibling;
// hydration stuff here...
}
function child() {
curr = curr?.firstChild;
// hydration stuff here...
}
return function() {
while (index < len) {
let char = chars[index++];
switch (char) {
case '>':
next();
break;
case '^':
child();
break;
case '|':
return curr;
}
}
}
}
}The encoding could also be shortened for repeated operations (for example, >3 could mean going to the third sibling). However, I think that the encoding should be relatively simple, because I, like probably a lot of other contributors, learned how Svelte's compiler and internals work by reading and analyzing the compiled output, and I wouldn't want this feature to make the compiled output too difficult for possible contributors to understand.
Additional Implementation Details
This would rewrite a bit of these files, but the changes shouldn't be too complex; instead of generating $.next, and $.child (and variables for each interim node), it would just push characters to something like context.state.sequence and generate calls to next. Additionally, since each sequence is unique to its template, I think it'd be best if the sequence were to be included as arguments to $.template, and fragment and next could be retrieved via destructuring:
let root = $.template(`<h1></h1><p></p>`, `|>|^|`, 1);
//...
let [fragment, next] = root();Importance
nice to have