Why use setTimeout trigger effect rather than queueMicrotask
I found the update is always slow by 1 frame when I change state in the rAF callback:
//minimal case
const a = van.state(0);
van.dervie(() => (el.textContent = a.val));
requestAnimationFrame(async (time) => {
a.val = time;
await 0; // wait next microtask
console.log(time === el.textContent); // expect true but got false
});
I know it is because the setTimeout callback will be called after the render pipeline, but updating the DOM in time in the rAF callback is usual practice. I found that the other reactive systems usually update on the next microtask, which ensures that the update will occur before the render pipeline.
https://jsfiddle.net/akqjhcy6/ https://playground.solidjs.com/anonymous/01307c7d-763d-4d87-a00b-432f12b85936
So, is there any reason why setTimeout must be used?
TBH I'm not that familiar with requestAnimationFrame. I chose setTimeout since it's easier to understand.
My mental model of the state change is: when you change the state while rendering the current frame, you're planning what to show next. Otherwise, when you're rendering the current frame, and you can change what to render in the current frame, things might become messy (not sure if it can lead to infinite loop).
Also, state derivation might be used for non-rendering purposes, coupling with rendering-related function might not be a good idea.
This pic below clearly shows what happened when I executed the code below. The purple and green blocks are render tasks. Before them, you can see the rAF callback was called, which adds a microtask and 2 timer tasks. You can see that the side effect is always called after the render pipeline.
const state = van.state(0);
const double = van.derive(() => state.val * 2);
requestAnimationFrame(() => {
state.val = 1; // add timer task
Promise.resolve().then(() => expect(double.val).to.equal(0)); // add microtask
setTimeout(() => expect(double.val).to.equal(2)); // add timer task
});
In the pic below, we can see that the effect will trigger the recalculation style in the next frame, not the current frame, which is why we can not get the correct style in the code below, and that is why the mainstream frameworks push the effect into microtasks, not timer tasks.
const state = van.state(0);
const el = div({ style: () => `height: ${state.val}px` });
van.derive(
// not recalculate style yet, should wait to next frame
() => state.val === 100 && expect(el.style.height).to.equal('0px'),
);
state.val = 100;
van.add(document.body, el);
This diff between state and dom usually results in a high mental burden. I think turning timer tasks into microtasks is absolutely safe if you don't need to do something after rendering.
Based on your input, I feel that it probably makes more sense to change VanJS implementation by using Promise.resolve().then(updateDoms) instead of setTimeout(updateDoms). As you illustrated, the callback via Promise.resolve().then(...) will be scheduled faster (as a microtask) compared to setTimeout(...) (as a timer task). Moreover, Promise.resolve().then(...) has the FIFO guarantee. Specifically, if you update a state and then later calls Promise.resolve().then(...) or await 0, it's guaranteed that your callback can see the state changes there.
Let me know if the change makes sense to you. If so, I can release a new version of VanJS to change that.
@Tao-VanJS, why not just use queueMicrotask instead of Promise.resolve().then(...), it should be use less overhead
creating and destroying promises takes additional overhead both in terms of time and memory that a function which properly enqueues microtasks avoids
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide#enqueueing_microtasks
Promise.resolve().then has better compatibility, which is used in Vue.
queueMicrotask has less code size, and it is used in Solid and Svelte.
This is a trade-off; I prefer the latter.
FYI, I just released VanJS 1.6.0 which switched to use queueMicrotask. Thank you so much for the discussion!
@all-contributors please add @cubxx for ideas
@all-contributors please add @sirenkovladd for ideas
@Tao-VanJS Thanks for your work. I'm writing this comment solely to warn something.
In the case below, el.style.height remains 1px when the effect was called for the 2nd time. This is because update will be called after all effects have completed.
const s = van.state(1);
const el = div({
style: () => (
performance.mark('style update: ' + s.val),
`height: ${s.val}px`
),
});
const effect = () =>
performance.mark(`effect: ${s.val} ${el.style.height}`); // 1px
van.derive(effect);
queueMicrotask(() =>
performance.mark(`microtask: ${s.val} ${el.style.height}`), // 2px
);
van.add(document.body, el);
s.val = 2;
I'm not sure if this aligns with your expectations. Anyway, we can still use queueMicrotask to access the correct style.
My expectation in the new version is: if you update a state (e.g.: s.val = 2), and subsequently call queueMicrotask(callback), the callback will see the state changes. But if queueMicrotask(callback) is called before the state change, callback won't see the state change.
I think there is a regression when using vanX reactive? It seems not even simple () => state.key are reactive when using the latest version. Maybe in vanX package, the vanjs-core could be defined as a peer dependnecy?
I think there is a regression when using vanX reactive?
Hmm..., I'm not seeing any regression in VanX apps after the release. And I did make sure all VanX test cases pass before doing the release.
While trying to set up a repo to reproduce the issue, I have identified the root cause.
initially I had:
"vanjs-core": "1.5.5",
"vanjs-ext": "^0.6.3",
After upgrade:
"vanjs-core": "1.6.0",
"vanjs-ext": "^0.6.3",
Because vanjs-ext has on the latest version available in npm ("version": "0.6.3"):
"dependencies": {
"vanjs-core": "^1.5.5"
},
This created two instances of vanjs-core, one with v1.5.5 used by vanjs-ext and the other by my app. The solution was to remove node_modules and lock file then reinstall and the package was deduped on the same version. And now it's works correctly.