- From: Joe Pea <notifications@github.com>
- Date: Mon, 18 Dec 2023 18:38:01 -0800
- To: whatwg/dom <dom@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/dom/issues/1111/1862010262@github.com>
This problem can easily be solved by adding a new APi like `ChildObserver` which - is concerned only with the equivalent of `MutationObserver`'s `childList` option (in other words, `childObserver.observe(el)` is similar to `mutationObserver.observe(el, {childList: true})`) - would have a single option, `subtree`, the same as `MutationObserver`'s (in other words, `childObserver.observe(el, {subtree: true})` is similar to `mutationObserver.observe(el, {childList: true, subtree: true})`) - callbacks will be called in a way such that observations are always in the correct order within the [**_mutation observer microtask_**](https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask) (this means a `ChildObserver`'s callback may be called multiple times in within the microtask, such that when there are multiple `ChildObserver`s, all of their callbacks run such that the order in which mutation records are presented is guaranteed to be the same order in which they actually happened) This would not be a breaking change to `MutationObserver`, but a new API that would become the default way to observe `childList` (`subtree` or not) without records being presented in the incorrect order. We could then soft-deprecate MutationObserver `childList` (f.e. the articles in the community would write about the best practice of using `ChildObserver` instead, and websites like MDN would mark this clearly in the docs. It seems to me like this addition would be fairly trivial to add. The only difference is - instead of a list of queued observers with records per observer, - we'd have a list of mutation record tuples with each tuple associated with an observer - when a mutation happens, if the last tuple was for the current observer, append a new record to the tuple - otherwise append a new tuple for the current observer and add the record to the new tuple - in the mutation microtask, iterate all record tuples - for the current tuple, call the associated observer's callback with the tuple Done! This will prevent end users from ever having to spend time with these difficult-to-handle situations. The example from the [above codepen](https://codepen.io/trusktr/pen/oNqrNXE/3aad3bb7315877d00c7c42e3d77fed5a?editors=1010) would now look like very similar, with `MutationObserver` replaced with `ChildObserver`, and would simply work: ```html <x-el id="one"> one <x-el id="two"> two <x-el id="three"> three </x-el> </x-el> </x-el> ``` ```js class XEl extends HTMLElement { connectedCallback() { this.o = new ChildObserver(changes => { for (const change of changes) { if (change.addedNodes.length) console.log('added child, initialize:', change.addedNodes) if (change.removedNodes.length) console.log('removed child, cleanup:', change.removedNodes) } }) this.o.observe(this) } disconnectedCallback() { this.o.disconnect() } } customElements.define('x-el', XEl) setTimeout(() => { const t = three t.remove() one.append(t) }, 1000) ``` The broken output from MutationObserver is: ``` added child, initialize: NodeList [x-el#three] removed child, cleanup: NodeList [x-el#three] ``` The new correct output using ChildObserver will be: ``` removed child, cleanup: NodeList [x-el#three] added child, initialize: NodeList [x-el#three] ``` # Polyfill Maybe we can polyfill this by - each instance of the polyfilled `ChildObserver` class will use a `MutationObserver` for scheduling with `childList: true` (passing along `subtree: true` if provided) - the `MutationObserver` callbacks will schedule (if not already scheduled) a microtask that will run after the _mutation observer microtask_ - in the next microtask, we have all the MutationObservers and their records, and we can order them as needed to figure out the above tuples - finally we can run callbacks for `ChildObserver` based on those tuples Because `MutationObserver` ordering is not correct, it is impossible to run `ChildObserver` callbacks interleaved with `MutationObserver` callbacks, and it would not make sense. As with this polyfill, the native implementation will simply run a **_child observer microtask_** after the **_mutation observer microtask_**. The assumption is that all `MutationObserver` callbacks work as-is, nothing breaks there, and `ChildObserver` behavior is entirely new, operates separately in its new microtask. -- Reply to this email directly or view it on GitHub: https://github.com/whatwg/dom/issues/1111#issuecomment-1862010262 You are receiving this because you are subscribed to this thread. Message ID: <whatwg/dom/issues/1111/1862010262@github.com>
Received on Tuesday, 19 December 2023 02:38:07 UTC