- From: Justin Fagnani <notifications@github.com>
- Date: Wed, 09 Apr 2025 09:55:20 -0700
- To: WICG/webcomponents <webcomponents@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <WICG/webcomponents/issues/1055/2790384808@github.com>
justinfagnani left a comment (WICG/webcomponents#1055) Control structure fit very naturally into this model, as they already fit into implicit schedulers that use the microtask queue: A parent component's update task may conditionally render a child component. That child component reacts by scheduling it's own update task, which may conditionally render a grandchild component and so on. Right now you can accomplish this by pushing update tasks onto the microtask queue. As long as the initial state change happens at the top of a subtree, then you'll get top-down updates for both existing components that need to update, and new components that need to initially render. So control structures are just fine. The problem happens when you get multiple incoming state changes at multiple levels of the tree. A child might be the first to react to the change, and schedule its update. Then a parent might react, and schedule its update after the child, so you get a queue of `[childTask, parentTask]`. With a naive queue approach that runs `childTask` first, running `parentTask` will cause a second `childTask` to be scheduled, and the child will update twice. If we can take this queue: `[childTask, parentTask]` and run it in this order:`[parentTask, childTask]` then we solve that problem. ### Tree mutations As for @smaug----'s question about tree mutations: There's at least one theoretically pure answer, which might or might not be expensive to implement: At every point in time there is a total ordering of tasks which matches the breadth-first traversal of the tree the tasks are associated with. After both adding tasks and mutating the tree there is still that total ordering. If you always pick the highest priority task by that order, then you get proper handling of tree mutations. There's an important question of how to handle tasks that trigger a new task of an ancestor, which could lead to starvation. That is generally very bad component behavior, so maybe it could be banned. Another approach is to say that within this ordering there is always a successor to the current task, and the successor is always the next task run, even if tree mutations and added tasks have enqueue tasks that precede the current task. If the task queue is not empty when there is no successor, then the highest priority remaining task by tree order is run. I think this side-steps the starvation problem, though it may still be desirable to ban tasks scheduling tasks higher on the tree. ### Timing For @rniwa's question about timing: I would _love_ to have synchronous timing if it's at all feasible. Batched but synchronous updates would solve a lot of problems we currently have with reactive components. We need batching, and right now the only ergonomic, interoperable, and roughly top-down ordered way we can get it is to use the microtask queue. So we trade synchronous updates for batching. But async rendering has some downsides. Component users can't easily know when a state change, like setting an attribute, will have been fully reflected in the DOM. This causes friction with code that does measurement, snapshotting, etc. It also causes us to make hard tradeoffs on when to do initial rendering and shadow root creation in web components that can lead to inconsistencies in slotting and event paths. The ideal timing would be that a task is executed synchronously, unless it is scheduled from within another task, in which case it's executed after the outer task. So: ```ts console.log('outer A'); parentElement.queueDOMUpdate(() => { console.log('parent A'); childElement.queueDOMUpdate(() => { console.log('child'); }); console.log('parent B'); }); console.log('outer B'); ``` would log: ``` outer A parent A parent B child outer B ``` This would mean that elements fullly update, in scenarios like: ```ts element.foo = 'bar'; // element's DOM is finished updating, even if children needed to update ``` But that we also get batching if an element needs to update multiple children and change multiple pieces of state on an element, because that runs inside a batch: ```ts // In element's class: this.queueDOMUpdate(() => { child.foo = this.foo; child.bar = this.bar; }); ``` In this case, `child` calls `this.queueDOMUpdate()` twice. There's only one task enqueued for it, and it runs right after `element`'s DOM update finished. Any code, like top-level script code or event handlers, that wants to ensure that multiple DOM updates are batched can call `document.queueDOMUpdate()`, or `element.queueDOMUpdate()`, like: ```ts element.addEventListener('click', () => { element.queueDOMUpdate(() => { element.foo = 1; element.bar = 2; }); }); ``` -- Reply to this email directly or view it on GitHub: https://github.com/WICG/webcomponents/issues/1055#issuecomment-2790384808 You are receiving this because you are subscribed to this thread. Message ID: <WICG/webcomponents/issues/1055/2790384808@github.com>
Received on Wednesday, 9 April 2025 16:55:24 UTC