Component lifecycle
Every component goes through the same stations: register → init → mounted → unmount. The core tracks each instance in a registry; the observer keeps the registry in sync with the DOM.
The flow
register ──▶ init ──▶ mounted ──▶ unmount
│ │ │ │
│ │ │ └─ teardownInstance():
│ │ │ listeners + __ngProbe, once
│ │ └─ instance tracked in the registry
│ │ (uid, element, listeners)
│ └─ initX(scope) runs, core assigns data-ng-uid
└─ ng.registerComponent('x', initX) at module load
| register | Each ng_<comp>.js calls ng.registerComponent(name, initX) at module load. The registry stores the init function and an empty instance map. |
|---|---|
| init | ng.init(scope) walks the registry and runs every initX(scope). Components only pick up roots without data-ng-uid; the core then assigns the uid and records the instance (element, uid, tracked listeners). |
| mounted | Listeners are live. Per-instance APIs sit on the root (root.__ng<Name>), global ones on window.ng.<name>. |
| unmount | ng.unmount(scope) tears down every instance whose root is inside the scope: tracked listeners are removed, __ngProbe.teardown() runs once, the instance leaves the registry. |
Boot
The initial mount of markup already in the page is done by ng_core.js at DOMContentLoaded — not by the observer. When everything is mounted the core dispatches ng:ready on document.
function bootNG() {
initState();
window.ng.init(); // mounts everything already in the DOM
window.ng.observer?.enable?.({ cleanRegistry: true });
}
// ng_core.js — runs at DOMContentLoaded (or immediately if already complete)
Runtime DOM changes — the observer
ng_observer.js is a MutationObserver for markup that enters or leaves the DOM after boot: inserted nodes are queued, compressed (descendants of a queued ancestor are skipped) and mounted in a single rAF-batched flush; removed nodes are unmounted; cleanRegistry() runs throttled to drop orphans. Without it, dynamically injected partials never mount.
One subtlety protects portal components — a node that is moved is not removed:
// ng_observer.js — _flushRemoved()
nodes.forEach(removed => {
// Skip nodes still connected: they were MOVED (portal, reorder),
// not actually removed. Unmounting here would kill the listeners
// of perfectly alive components (e.g. popover.open() → body).
if (document.contains(removed)) return;
window.ng?.unmount?.(removed);
});
Teardown
All unmount paths (ng.unmount, ng.removeComponent, cleanRegistry) funnel into one helper:
function teardownInstance(inst) {
removeListeners(inst); // everything tracked via u.listen
try { el?.__ngProbe?.teardown?.(); } // non-listener resources, once
catch {}
delete el.__ngListeners;
delete el.__ngProbe;
}
This is why listeners must always be registered via u.listen(...) — a direct addEventListener is invisible to the registry and survives the unmount, leaking. Non-listener resources (timers, observers, channels) go in __ngProbe — see the Component contract.
API
ng.mount(scope, name?) // mount one component (or all) inside scope
ng.unmount(scope, name?) // teardown instances whose root is scope or inside it
ng.removeComponent(name, uid?, removeFromDOM?)
ng.cleanRegistry() // drop instances whose element left the document
Related
ng:ready data-ng-uid __ngListeners __ngProbe ng.observer.enable() ng.observer.disable()