- Introduction
- Precompiler Overview
- Runtime Overview
- References
- Validators
Runtime CompilerInitial RenderRerendering (Updating)The EnvironmentOptimizations
The core primitive of the Glimmer runtime is the Reference
abstract data
type.
Fundamentally, a reference is a stable object that represents the result of a pure (side-effect-free) computation, where the result of the computation might change over time. It has the following interface:
interface Reference<T> {
value(): T;
}
If you are familiar with FRP terminologies, you might recognize this as a
discrete "signal". The key departure from other similar constructs in other
libraries (such as Ember's Stream
s and ReactiveX's Observable
s) is that
references is a pull-based system with no notion of "subscriptions" or
"notifications".
As explained in the previous chapter, we find a pull-based system to be a better fit (and ultimately more efficient) for the kind of problems we are trying to solve. (In the next chapter, we will discuss a technique for tracking changes without notifications.)
In the following example, we will construct a simple reference that "captures"
the value for the foo
variable over its lifetime:
let foo = 1;
let fooReference: Reference<number> = {
value() {
return foo;
}
};
fooReference.value(); // => 1
foo++;
fooReference.value(); // => 2
As you can see, calling fooReference.value()
will always yield the current
value of the foo
variable.
This is a fairly basic example, but it begins to illustrate the power of the
Reference
abstraction. While JavaScript variables always contain values
that could be passed around to (and held onto by) other functions, the
variable bindings themselves are not a first-class value. Using the reference
system, it is trivial to pass a variable by reference (hence the name for the
Reference
data type).
Reference
s are inherently composable. In this example, we will model the
foo + bar
computation using Reference
s:
let foo = 1;
let bar = 2;
let fooReference: Reference<number> = {
value() {
return foo;
}
};
let barReference: Reference<number> = {
value() {
return bar;
}
};
let fooPlusBarReference: Reference<number> = {
value() {
return fooReference.value() + barReference.value();
}
};
fooPlusBarReference.value(); // => 3
foo = 2;
fooPlusBarReference.value(); // => 4
bar = 3;
fooPlusBarReference.value(); // => 5
As you can see, fooPlusBarReference
composes fooReference
and
barReference
instead of accessing the variables directly. As foo
and bar
change over time, the fooPlusBarReference
stays up-to-date and returns the
correct result of foo + bar
.
Because Reference
s are so composable, it is also very easy to write some
higher-order combinators to model some common operations. For example, we can
generalize fooPlusBarReference
into a reusable AdditionReference
class:
class AdditionReference implements Reference<number> {
private lhs: Reference<number>;
private rhs: Reference<number>;
constructor(lhs: Reference<number>, rhs: Reference<number>) {
this.lhs = lhs;
this.rhs = rhs;
}
value(): number {
return this.lhs.value() + this.rhs.value();
}
}
Another example is the "map" operation:
// A `Mapper` is a function that takes a value of type `T` and returns a new
// value of type `U`.
type Mapper<T,U> = (T) => U;
function map<T,U>(source: Reference<T>, mapper: Mapper<T,U>): Reference<U> {
return new MapperReference(source, mapper);
}
class MapperReference<T, U> implements Reference<U> {
private source: Reference<T>;
private mapper: Mapper<T,U>;
constructor(source: Reference<T>, mapper: Mapper<T,U>) {
this.source = source;
this.mapper = mapper;
}
value(): U {
let { source, mapper } = this;
return mapper(source.value());
}
}
let foo = 4919;
let fooReference: Reference<number> = {
value() {
return foo;
}
};
// Converts a number into its hexidecimal (base 16) representation
let toHexMapper: Mapper<number, string> = function(num) {
return '0x' + num.toString(16).toUpperCase();
};
let hexReference = map(fooReference, toHexMapper);
hexReference.value(); // => '0x1337'
foo = 49374;
hexReference.value(); // => '0xC0DE'
Since references are pull-based, it is trivial to implement lazy evaluation
semantics simply by avoiding calling .value()
until it is necessary. Consider
this naïve implementation of a reference that models the ternary conditional
expression in JavaScript (condition ? ifTrue : ifFalse
):
class ConditionalExpressionReference<T> implements Reference<T> {
private predicate: Reference<boolean>;
private consequent: Reference<T>;
private alternative: Reference<T>;
constructor(predicate: Reference<boolean>, consequent: Reference<T>, alternative: Reference<T>) {
this.predicate = predicate;
this.consequent = consequent;
this.alternative = alternative;
}
value(): T {
let predicate = this.predicate.value();
let consequent = this.consequent.value();
let alternative = this.alternative.value();
return predicate ? consequent : alternative;
}
}
let dayOfWeek = 'Friday';
let isWorkDay: Reference<boolean> = {
value() {
return dayOfWeek !== 'Saturday' && dayOfWeek !== 'Sunday';
}
};
let work: Reference<string> = {
value() {
let result = [];
result.push('Working...');
result.push('Working...');
result.push('Working...');
result.push('(X_X)');
return result.join(' ');
}
};
let relax: Reference<string> = {
value() {
return 'Relaxing... (v_v)'
}
};
let result = new ConditionalExpressionReference(isWorkDay, work, relax);
result.value(); // => 'Working... Working... Working... (X_X)'
dayOfWeek = 'Saturday';
result.value(); // => 'Relaxing... (v_v)'
While this implementation works, it eagerly evaluates both the "consequent" and "alternative" references, even though only one of the two values are used. This is not ideal, because the references might represent arbitrarily expensive computations.
Instead, we can change the implementation to evaluate the references lazily (also known as "short-circuit evaluation" in this case):
class ConditionalExpressionReference<T> implements Reference<T> {
// ...
value(): T {
let { predicate, consequent, alternative } = this;
if (predicate.value()) {
return consequent.value();
} else {
return alternative.value();
}
}
}
In this improved implementation, it is guaranteed that only one of the two clauses is evaluated, eliminating a wasteful and potentially expensive computation.
References play a very important role in the Glimmer templating system.
When Glimmer renders a template, each dynamic segment (such as the {{foo}}
in
<b>{{foo}}</b>
) are represented by a single reference. On initial render,
these dynamic segments are populated by pulling an initial value()
out of
these references.
These references also allow the templates to be re-rendered later with the
most-current data, simply by pulling the latest value()
out of each reference
and updating the DOM nodes correspondingly (we will discuss the second part
later).
References also help bridge the gap between the "impure" (effectful) parts of the system from the "pure" (functional) part of the system.
In Handlebars, a template is always rendered against a "context" (typically
known as "self" inside Glimmer), similar to JavaScript's this
when invoking
a function. Take the following template as an example:
Assuming motd
is not a helper, both of the dynamic segments are describing a
path lookup on the context (sometimes called a "self lookup" inside Glimmer).
That is, {{user.name.first}}
is referring the value of this.user.name.first
where this
is the context object. In fact, they can be rewritten as
{{this.user.name.first}}
and {{this.motd}}
for clarity.
Since it is possible for the context to change from one object to a different object between re-renders, the context itself is modeled as a reference. Because Handlebars supports arbitrary path lookups on the context (as we saw in the previous example), Glimmer needs a way to create additional references from the context reference for a given path.
Here is one possible solution:
// Encodes the "soft fail" path lookup semantics in Handlebars
//
// Usage:
// let obj = { foo: { bar: 'baz' } };
// get(obj, 'foo') => { bar: 'baz' }
// get(obj, 'foo, 'bar') => 'baz'
// get(obj, 'foo, 'nope') => undefined
// get(obj, 'foo, 'bar', 'baz') => undefined
function get(object: any, ...subpaths: string[]) {
if (subpaths.length === 0) {
return object;
}
if (object && typeof object === 'object') {
let head = subpaths[0];
let tail = subpaths.slice(1);
return get(object[head], ...tail);
}
}
class PathLookupReference implements Reference<any> {
private context: Reference<any>;
private subpaths: string[];
constructor(context: Reference<any>, path: string) {
this.context = context;
this.subpaths = path.split('.');
}
value(): any {
return get(this.context.value(), ...this.subpaths);
}
}
let context = {
user: { name: { first: 'Godfrey', last: 'Chan' } },
motd: 'Welcome back!'
}
let contextReference: Reference<any> = {
value() {
return context;
}
};
// {{user.name.first}}
let firstName = new PathLookupReference(contextReference, 'user.name.first');
// {{motd}}
let motd = new PathLookupReference(contextReference, 'motd');
firstName.value(); // => 'Godfrey'
motd.value(); // => 'Welcome back!'
context.user.name = { first: 'Yehuda', last: 'Katz' };
firstName.value(); // => 'Yehuda'
While this implementation works, because it evaluates the context reference into a value, the "parent" and "child" references are not connected in any meaningful way.
Occasionally, there might be extra information on the context object that you might want to propagate to the downstream references.
For example, the context object might be a simple primitive type like strings,
numbers, or undefined
, in which case all the subsequent path lookups will
yield undefined
.
Alternatively, the context object might be an immutable data structure, in which
case all of downstream value()
s do not need to be recomputed so long as
the context object itself did not get replaced.
In addition to the context objects, certain advanced features in Handlebars (and other extensions in host environments like Ember) inadvertently means that almost any references (such as the result returned by a helper) can be used in a path lookup position.
For all of these reasons, Glimmer defines an extension to the base Reference
type called a PathReference
:
interface PathReference<T> extends Reference<T> {
get(path: string): PathReference<any>;
}
In addition to the value()
method, PathReference
s support a get
method
that is responsible for converting these path lookups into a child reference.
This allows the parent reference to encode and propagates extra information
downwards.
A simple example is a reference containing a primitive value (such as a string,
a number, undefined
, etc):
const NULL_REFERENCE: PathReference<void> = {
value() {
return undefined;
},
get(path: string) {
return NULL_REFERENCE;
}
};
type Primitive = string | number | boolean | void;
class PrimitiveReference<T extends Primitive> implements PathReference<T> {
private innerValue: T;
constructor(value: T) {
this.innerValue = value;
}
value(): T {
return this.innerValue;
}
get(path: string): PathReference<void> {
return NULL_REFERENCE;
}
}
Since all subsequent path lookups on a primitive value will always yield
undefined
, PrimitiveReference
is able to take advantage of this information
and return a constant, specialized PathReference
in its implementation of
get()
.
Another example is the hash
helper in Ember, which takes the named arguments
and convert it into a "hash" (or "dictionary") object:
Inside the user-profile
component, options
can be accessed like a regular
property:
Here is a simplified implementation for the hash
reference:
class HashReference implements PathReference<Dictionary<any>> {
private args: Dictionary<PathReference<any>>;
constructor(args: Dictionary<PathReference<any>>) {
this.args = args;
}
value(): Dictionary<any> {
let dict = new Dictionary<any>();
Object.keys(this.args).forEach((name) => {
dict[name] = this.args[name].value();
});
return dict;
}
get(path: string): PathReference<any> {
return this.args[path] || NULL_REFERENCE;
}
}
By implementing the PathReference
interface, HashReference
can avoid
constructing the Dictionary
object (and evaluating all the unused references
in the process) to fulfill a simple path lookup (e.g. {{@options.me}}
in the
example above).