You call `observer.observe(el, { childList: true })` on a MutationObserver. Which changes under `el` will fire the callback, which will be ignored, and what happens if you pass `{}` instead?
answer
- options are a whitelist, nothing default-on
- three categories, one modifier
- subtree is not a category
- empty options is not "observe everything"
- text edits live on the text node
basics
~20 schildList: true reports only direct children of el being added or removed. Attribute edits, in-place text changes, and anything deeper in the tree are ignored unless you also pass attributes, characterData or subtree. Calling observe with {} throws a TypeError.
solid answer
~40 s`observe(el, { childList: true })` registers exactly one category of change on exactly one node: children being **added to or removed from `el` itself**. Setting an attribute on `el`, editing an existing text node's data, or appending a node three levels down produce no records. To widen it you opt in explicitly: `attributes: true` for attribute changes, `characterData: true` for in-place text edits, and `subtree: true` to apply whichever categories you enabled to every descendant as well. The options object is a whitelist, not a filter — nothing is on by default. If none of `childList`, `attributes` or `characterData` ends up true, `observe()` throws a `TypeError`, so `observe(el, {})` and `observe(el, { subtree: true })` both fail immediately rather than silently observing nothing.
code
javascript · 13 linesconst mo = new MutationObserver((records) => {
for (const r of records) console.log(r.type, r.target.nodeName);
});
const list = document.createElement('ul');
document.body.append(list);
mo.observe(list, { childList: true });
const li = document.createElement('li');
li.textContent = 'one';
list.append(li); // record: childList on UL
li.setAttribute('data-x', '1'); // no record: attributes not enabled
li.firstChild.data = 'two'; // no record: characterData + subtree not enabledgo deeper
Know that a MutationObserver watches nothing until observe(target, options) is called, and that you must name what you want: childList, attributes or characterData.
Be ready to explain each category precisely, why subtree is a modifier rather than a category, why empty options throw a TypeError, and why a text edit may or may not surface depending on how it was written.
Show that you choose the narrowest registration that answers the question, and can explain the cost and record volume that a broad childList+subtree+attributes registration imposes on a busy page.
Own the guidance for when DOM observation is the right integration seam at all versus owning the mutation path, and set the team convention for narrow targets, attributeFilter, and mandatory teardown.
## What observe() actually registers A `MutationObserver` is created with a callback and watches nothing until you register it: ```js const mo = new MutationObserver((records, observer) => { /* ... */ }); mo.observe(el, { childList: true }); ``` `observe(target, options)` creates a registration on `target` that says which *categories* of change should produce `MutationRecord` objects. The options object is a whitelist: every category you do not name is off. One observer may register on many nodes; every record from every registration arrives at the same callback, and you tell them apart with `record.type` and `record.target`. ## The three categories **`childList`** — child nodes of the observed node being inserted or removed. That covers `append`, `prepend`, `remove`, `replaceChildren`, `insertBefore`, and assignments to `innerHTML` or `textContent` (which remove the old children and insert new ones). The record carries `addedNodes`, `removedNodes`, `previousSibling` and `nextSibling`, with `target` set to the parent whose child list changed. **`attributes`** — an attribute set or removed on an observed element. The record carries `attributeName` and `attributeNamespace`, with `target` set to the element. Note that the record tells you *which* attribute changed, not the new value — you read that back off `target` yourself. **`characterData`** — the data of a `CharacterData` node (a `Text` or `Comment` node) changing in place, as in `node.data = 'x'`. The record's `target` is the text node itself, not its parent element. ## subtree widens whatever you enabled `subtree: true` is a modifier, not a category. It means "apply the categories I enabled to this node and every descendant". `{ childList: true, subtree: true }` therefore reports insertions and removals anywhere below the target, with `target` naming the specific parent involved. `{ subtree: true }` alone still enables nothing, which is why it throws. ## At least one category is mandatory The DOM specification requires that `childList`, `attributes` or `characterData` be true, and `observe()` throws a `TypeError` otherwise. Two conveniences soften that rule: if you pass `attributeOldValue` or `attributeFilter` without mentioning `attributes`, `attributes` is treated as true; likewise `characterDataOldValue` implies `characterData`. But explicitly combining `attributes: false` with `attributeFilter`, or `characterData: false` with `characterDataOldValue`, is a `TypeError` — the spec refuses contradictory options rather than guessing. ## The text-node trap The most common surprise is observing an element for text changes: ```js mo.observe(p, { characterData: true }); // never fires p.firstChild.data = 'updated'; ``` The mutation happens on the `Text` node, which is a *child* of `p`, so the registration on `p` does not cover it. Either observe the text node directly, or use `{ characterData: true, subtree: true }` on `p`. Confusingly, `p.textContent = 'updated'` *does* fire an observer with `{ childList: true }`, because that assignment replaces the child text node rather than editing it — same visible outcome, different mutation category. ## Re-registering and stacking options Calling `observe()` a second time for the same node with the same observer does not add a second registration: it **replaces** the options for that node. So this is a bug: ```js mo.observe(el, { childList: true }); mo.observe(el, { attributes: true }); // childList is now OFF for el ``` If you want both, ask for both in one call: `mo.observe(el, { childList: true, attributes: true })`. ## Delivery is asynchronous Records are queued and handed to your callback later as an array, never synchronously during the DOM write. Code that mutates and then immediately inspects a counter the callback increments will see the old value. Reach for `takeRecords()` when you need whatever is queued right now. ## Practical shape Pick the narrowest options that answer your question. `{ childList: true }` on a specific list container is cheap and precise; `{ childList: true, subtree: true, attributes: true }` on `document.body` sees every change in the application and pays for all of them. When you only care about one attribute, add `attributeFilter: ['data-state']` so unrelated attribute writes never allocate a record at all.
- If you call observe() twice on the same element with different options, does the observer end up watching the union of both?No. A second `observe()` call for the same node with the same observer replaces the previous registration's options rather than merging them, so the earlier categories are switched off. Ask for everything you need in a single call: `mo.observe(el, { childList: true, attributes: true, subtree: true })`. Registering the *same* observer on a *different* node does add a registration — one observer can watch many targets, and its callback receives records from all of them.
- An element's text is updated and your { childList: true } observer fires, even though nobody added or removed an element. Why?Assigning to `textContent` (or `innerHTML`) does not edit the existing text in place — it removes the node's current children and inserts a fresh text node. That is a child-list mutation, so it produces a record with populated `removedNodes` and `addedNodes`. Editing `node.firstChild.data` instead mutates the text node itself and needs `characterData` plus `subtree` to be seen.
- How do you tell which of several observed elements a record came from?Read `record.target`. Each `MutationRecord` names the node the mutation happened on: the parent element for `childList`, the element for `attributes`, and the `CharacterData` node itself for `characterData`. Combined with `record.type`, that is enough to route records from a single observer registered on many nodes. The callback's second argument is the observer, which is handy for disconnecting from inside the callback.
Think of observe() as subscribing to specific mailing lists rather than to "all mail": you get exactly the lists you tick, and ticking none is rejected as a mistake rather than treated as ticking all.
saying these in an interview costs you the question
- Assumes subtree is enabled by default
- Thinks childList also reports attribute changes
- Expects observe(el, {}) to observe everything
- Believes the callback runs synchronously with the DOM write
- Watches an element for characterData without subtree
- Calls observe() repeatedly expecting options to accumulate