Glossary

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

lifecycle
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
registerEach ng_<comp>.js calls ng.registerComponent(name, initX) at module load. The registry stores the init function and an empty instance map.
initng.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).
mountedListeners are live. Per-instance APIs sit on the root (root.__ng<Name>), global ones on window.ng.<name>.
unmountng.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.

ng_core.js
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
// 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:

ng_core.js
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_core.js
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()