How a component resolves the names an expression uses.
Why this is its own object
Now that expressions are compiled at build time, the scope is what remains of the compiler/runtime boundary inside the runtime: the compiler emits closures that take one argument, and this is the thing it is given. Everything about what a component's expressions can see — its state, its computed values, its actions, its props, its bridges, its resources, the values injected into it, the names its own module imported — is decided here and nowhere else.
It used to be four private methods and a cache field spread across a 2,900-line class, which made two things hard to see. It hid the ordering rule, which is load-bearing and easy to get wrong. And it hid the fact that a scope was being constructed per evaluation: on the compiled path that is one allocation and one layer walk per binding, per update, for an object whose contents almost never change.
Precedence, and why it is what it is
Layers are consulted highest first:
- per-call extras — a
<@for>item, anevent, an action'sargs - injected values (
provide/inject) - framework names —
props,styles,$route,$emit,$watch, … - the component's own actions
- state and computed values, read from the live reactive proxy
- resources
- bridges
- mixin properties
stateitself- the names the component's module imported
Bridges sit below the component's own declarations so a bridge cannot
silently shadow a <state> key or an action; the compiler reports such a
collision separately. Imports sit last because an import is the least
specific thing in scope.
Nothing is read until it is named
The layers are a Proxy, not a merged object. Spreading the reactive state
into a plain object — which is what this replaced — read every key eagerly,
which made bare identifiers non-reactive, reported cycles that did not exist,
and tied every render to every state key. A get here resolves through the
layers and, for a state or computed name, reads the live proxy inside
whichever watcher is evaluating, so an expression depends on exactly the
names it mentions.
Caching
The base scope — the one with no per-call extras — is built once and reused. It is invalidated when something changes what a name resolves to: the set of state keys, the action map, the injected values. A scope with extras derives from it rather than rebuilding, which is why deriveScope exists: a spread would reintroduce the eager read this whole design removes.
- Source: