Skip to content

Commit

Permalink
⚡️ Group reactive tasks & skip the ones that are unnecessary
Browse files Browse the repository at this point in the history
  • Loading branch information
skerit committed May 5, 2024
1 parent ef21950 commit 8484f0a
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 4 deletions.
123 changes: 120 additions & 3 deletions lib/core/renderer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const TASK_GROUP = Symbol('task_group'),
APPLIED_OPTIONS = Hawkejs.APPLIED_OPTIONS,
SCOPE_ID = Symbol('scope_id'),
OPTIONS = Symbol('options');
OPTIONS = Symbol('options'),
REACTIVE_QUEUE = Symbol('reactive_queue');

/**
* The Renderer class
Expand Down Expand Up @@ -78,6 +79,7 @@ const Renderer = Fn.inherits('Hawkejs.Base', function Renderer(hawkejs) {
this.active_variables = null;
this.state = null;
this.compiled_inlines = {};
this.reactive_queue = null;
});

Renderer.setDeprecatedProperty('assign_end', 'assignEnd');
Expand Down Expand Up @@ -131,6 +133,33 @@ Renderer.setStatic(function enforceRootProperty(key, fnc) {
});
});

/**
* Set a method that only executes on the root renderer
*
* @author Jelle De Loecker <[email protected]>
* @since 2.4.0
* @version 2.4.0
*
* @param {String} key
* @param {Function} fnc
*/
Renderer.setStatic(function setRootMethod(key, fnc) {

if (typeof key == 'function') {
fnc = key;
key = fnc.name;
}

this.setMethod(key, function rootMethod(...args) {

if (!this.is_root_renderer) {
return this.root_renderer[key](...args);
}

return fnc.call(this, ...args);
});
});

/**
* Set a reference to a specific singleton element
* (html, head, body)
Expand Down Expand Up @@ -176,7 +205,9 @@ Renderer.setStatic(function attachReactiveListeners(element, reactive) {
}

if (reactive.body?.values?.length) {
reactive.body.values.forEach(optional => optional.onChange(() => reactiveRerender(element)));
reactive.body.values.forEach(optional => optional.onChange(() => {
queueReactiveTask(element, 'body', () => reactiveRerender(element));
}));
}

if (reactive.attributes) {
Expand Down Expand Up @@ -230,11 +261,32 @@ const attachReactiveElementUpdaters = (element, setter, getter, instructions) =>
}

config.values.forEach(optional => optional.onChange(() => {
return updateElementProperty(element, setter, getter, optional, key, config)
return queueReactiveTask(element, 'property', () => updateElementProperty(element, setter, getter, optional, key, config));
}));
}
};

/**
* Queue the given task
*
* @author Jelle De Loecker <[email protected]>
* @since 2.4.0
* @version 2.4.0
*
* @param {HTMLElement} element
* @param {string} type
* @param {Function} task
*/
const queueReactiveTask = (element, type, task) => {

// Make sure we have all the required info for a rerender
if (type == 'body' && !element.is_custom_hawkejs_element && !element[Hawkejs.RENDER_INSTRUCTION]) {
return;
}

return element.hawkejs_renderer.queueReactiveTask(element, type, task);
};

/**
* Perform the given reactive property update
*
Expand Down Expand Up @@ -942,6 +994,71 @@ Renderer.setMethod(function toDry() {
return result;
});

/**
* Queue a reactive task
*
* @author Jelle De Loecker <[email protected]>
* @since 2.4.0
* @version 2.4.0
*
* @param {HTMLElement} element The element in question
* @param {string} type What will be affected: property or body
* @param {Function} task The actual task
*/
Renderer.setRootMethod(function queueReactiveTask(element, type, task) {

if (!this[REACTIVE_QUEUE]) {
this[REACTIVE_QUEUE] = [];

Blast.nextGroupedImmediate(() => {
// Get the current queue
let queue = this[REACTIVE_QUEUE];

// Remove the queue so new tasks can be added
this[REACTIVE_QUEUE] = null;

// Now we have to take a look at all the queued elements.
// Any element that has a task queued while it's in another element
// that will completely rerender its contents should be skipped
let allowed_queue = [],
elements_to_rerender = new Set();

// First pass: get all the elements to re-render
for (let item of queue) {
if (item.type == 'body') {
elements_to_rerender.add(item.element);
}
}

// Second pass: skip elements inside elements that will be re-rendered
for (let item of queue) {
let is_allowed = true;

for (let ancestor of elements_to_rerender) {
if (ancestor != item.element && ancestor.contains(item.element)) {
is_allowed = false;
break;
}
}

if (is_allowed) {
allowed_queue.push(item);
}
}

for (let item of allowed_queue) {
try {
item.task.call(this, item.element);
} catch (err) {
console.error(err);
}
}
});
}

this[REACTIVE_QUEUE].push({element, task, type});
});

/**
* Convert to JSON
*
Expand Down
134 changes: 133 additions & 1 deletion test/10-expressions.js
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,8 @@ This should be a converted variable:

describe('Reactive variables', () => {

let state;

let tests = [
[
(vars) => vars.set('ref_title', Optional('Original title')),
Expand Down Expand Up @@ -952,6 +954,132 @@ This should be a converted variable:
ref_el.value = el;
},
`<span>CHANGED</span><div>CHANGED</div>`,
],
[
// Prepare the state & variables
(vars) => {
state = {};
vars.set('ref_el', Optional()).onChange(val => state.last_ref_el = val);
vars.set('ref_attr', Optional('-'));
vars.set('ref_static', Optional('static'));
state.ref_counter = vars.set('ref_counter', Optional(1));
},
// The initial test template (first string is always the template)
`
<div>
{{ &ref_static }}
<span :ref={% ref_el %} data-attr={% &ref_attr %}>
{{ ref_counter }}
</span>
</div>
`,
// The expected result
`
<div>
static
<span data-attr="-">
1
</span>
</div>
`,
// New function to change things
(vars) => {
state.current_span = state.last_ref_el;
vars.get('ref_attr').value = 'changed!';

// Even though we change it, it should not trigger a rerender
// since the variable was not used reactively in the template
vars.get('ref_counter').value = 2;
},
// New expected result
`
<div>
static
<span data-attr="changed!">
1
</span>
</div>
`,
(vars) => {
// The reference element should still be the same
// (The first div should not have re-rendered its contents)
assert.strictEqual(state.last_ref_el, state.current_span);
},
],
[
// Prepare the state & variables
(vars) => {
state = {};
vars.set('ref_static', Optional('static'));
state.ref_counter = vars.set('ref_counter', Optional(1));
state.info_counter = vars.set('info_counter', Optional(0));
},
// The initial test template (first string is always the template)
`
<div>
{{ &ref_static }}
<span>
<% info_counter.value += 1 %>
Info counter: {{ &ref_counter }}
<span>
<% info_counter.value += 1 %>
Nested info counter: {{ &ref_counter }}
</span>
Text
</span>
</div>
`,
// The expected result
`
<div>
static
<span>
Info counter: 1
<span>
Nested info counter: 1
</span>
Text
</span>
</div>
`,
(vars) => {
assert.strictEqual(state.info_counter.value, 2, 'The info counter should have been increased twice');
state.ref_counter.value = 2;
},
// New expected result
`
<div>
static
<span>
Info counter: 2
<span>
Nested info counter: 2
</span>
Text
</span>
</div>
`,
(vars) => {
let value = state.info_counter.value;
assert.strictEqual(value, 4, 'The info counter should have been increased twice to four, but it is ' + value);
state.ref_counter.value = 3;
},
`
<div>
static
<span>
Info counter: 3
<span>
Nested info counter: 3
</span>
Text
</span>
</div>
`,
]
];

Expand Down Expand Up @@ -1049,7 +1177,11 @@ function createTests(tests) {
}
}

title = code.replace(/\r\n/g, '\\n').replace(/\n/g, '\\n').replace(/\t/g, '\\t');
//title = code.replace(/\r\n/g, '\\n').replace(/\n/g, '\\n').replace(/\t/g, '\\t');
title = code.trim().replace(/\r\n/g, ' ').replace(/\n/g, ' ').replace(/\t/g, ' ');

// Replace multiple whitespaces with a single space
title = title.replace(/\s+/g, ' ');
} else {
title = test.template;
template = test.template;
Expand Down

0 comments on commit 8484f0a

Please sign in to comment.