This repository was archived by the owner on Apr 4, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 140
Idempotent rerender #318
Merged
Merged
Idempotent rerender #318
Changes from all commits
Commits
Show all changes
93 commits
Select commit
Hold shift + click to select a range
6777502
Add ability for templates to reuse morphs
856984e
Encapsulate state needed for re-render
b98ea5a
Extract static cached fragment logic to runtime
tomdale 389becc
Break apart hydration code into phases
tomdale 4524d58
Introduce element morphs
tomdale c7a4af7
Fix failing tests
tomdale f491efd
renderNodes -> buildRenderNodes
9221a17
Guard if no parent and no contextualElement
6de350f
Expose public API for easy rerendering
95d795d
Block helpers mutate in place rather than return
fa3db79
Rename rerender to revalidate
5841c13
Simplify template rendering in block helpers
58e9af6
Propagate ownerNode through the render nodes
3845ac9
Maintain consistent “shape” for options hash
3867808
`get` helper should get render node for dirtying
766d807
Introduce dirtiness to render node invalidation
c3315f6
Defer more of rendering to the runtime
wycats df06837
Remove unnecessary inlined render function
wycats 102bc6a
Use extracted morph-range library
7a86d88
Ensure templates always have stable boundaries
2d966c3
Clean up scope handling
wycats 54a4436
Make ordering of scope/env consistent
wycats 1469fbf
Tighten up argument order
906bc5e
Provide `this.yield` sugar in block helpers
fbcdf48
Hooks don't need to understand contextualElement
67f10c0
Start writing host hook documentation
d27c910
Implement dirtying render with layouts
wycats 0f69968
Factor the hooks better
wycats b6779d1
Move dirty checking out of the hooks
wycats d1f0773
Don't defer yielding template anymore
wycats 7b85d7c
Implement `yieldItem` for #each helper
eea6af7
Try to make more of the hot path static
wycats 9ac6894
Missing file
wycats 21e0031
Make sure to walk through MorphLists
09471b7
Provide a hook to normalize values
8d4f548
Compatibility with more downstream builds
c1730ab
Improve Ember integration
90fe5c1
linkRenderNode can cache last linked values
e1bd02c
Cleanup pruned render nodes
acbab7a
Inline helpers can insert new top-level templates
739e18b
Make it possible to destroy a top-level morph
e133d60
Always revalidate child nodes for all statements
8c9634e
Properly propagate visitor through hooks
838ccdd
Avoid revalidating children twice
99d5bb3
Fix some bugs related to template passthrough
ed5ccab
Expose revalidation logic
156dac2
Make sure revalidation is properly recursive
1f71c6a
Allow hosts to create fresh scope directly
2d22fbf
Add a document explaining HTMLBars lifecycle
wycats fb303e6
Clarify language
wycats de5753e
Cleaner abstraction for common keyword patterns
9da2487
setupState & shouldPrune happen on initial render
24ebdf0
Pass in needed state to render keyword hook
ae859e6
Expand hooks for Ember integration
083d4af
Assorted cleanup
wycats 27195e0
Make sure `content` nodes support redirection
381c7b3
`hostBlock` takes options for `createShadowScope`
bf387f5
Fix a bug in yieldItem
bef607c
Expose just the block handling part of `block`
7555ebf
Make RenderResult an object
wycats 35cdb72
A few things to improve baseline performance
0562518
Add a description of a performance optimization
dca4418
Give host opportunity to handle scope extensions
d443a67
Rename internal pruneMorph method to clearMorph
d922530
Always invoke `bindScope()` hook
9c56290
Extract dirtiness check and add subtree dirtiness
wycats 999a838
Pass env to `bindSelf` and `bindScope`
tilde-engineering 60f33e0
Better abstractions for multi-level blocks
af6e7e7
Simplify nested blocks
wycats 9fb2b27
Export blockFor as a public utility (Title 2)
4c10607
Pass more information to yielded blocks
51123cc
Make blockFor and manualElement more flexible
4079f76
Fix bug
d35ceb2
Upgrade to latest morph-range
tilde-engineering b24e3ed
Pass env to linkRenderNode
tilde-engineering 2bb3fdf
Keyword cleanup
tilde-engineering 550312c
Expose new morph creation methods
d41095b
Let yieldItem support self bindings
mmun e7e9893
Extract invokeHelper and keyword hooks
a8bfdd7
Implement a few more scope-related hooks
5d0d86d
Differentiate clearing and destroying
f73c1eb
Allow Morph types to be subclassed
wycats 42b9bd5
Implement a built-in default stability check
wycats 56d6a9b
Improve child env support
wycats 4146d66
Export clearMorph for framework cleanup
ad6bf44
Improve cleanup
1b993fe
Element modifiers support keywords
mmun 86b6d48
Tweak tests to be more stable across browsers
mmun 065680b
Use array utils to support old IE
mmun d8223db
Use nodeValue instead of textContent to support old IE
mmun 7b93313
Remove trailing commas
mmun 8fe4bdd
Fix module import
mmun 85058de
Use only HTML4 tags in tests
mmun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
An HTMLBars runtime environment implements a series of hooks (and | ||
keywords) that are responsible for guaranteeing the most important | ||
property of an HTMLBars template: idempotence. | ||
|
||
This means that a template that is re-rendered with the same dynamic | ||
environment will result in the same DOM nodes (with the same identity) | ||
as the first render. | ||
|
||
HTMLBars comes with support for idempotent helpers. This means that a | ||
helper implemented using the HTMLBars API is guaranteed to fulfill the | ||
idempotence requirement. That is because an HTMLBars template is a "pure | ||
function"; it takes in data parameters and returns data values. | ||
|
||
> Block helpers also have access to `this.yield()`, which allows them to | ||
> render the block passed to the block helper, but they do not have | ||
> access to the block itself, nor the ability to directly insert the | ||
> block into the DOM. As long as `this.yield()` is invoked in two | ||
> successive renders, HTMLBars guarantees that the second call | ||
> effectively becomes a no-op and does not tear down the template. | ||
|
||
HTMLBars environments are expected to implement an idempotent component | ||
implementation. What this means is that they are responsible for | ||
exposing a public API that ensures that users can write components with | ||
stable elements even when their attributes change. Ember.js has an | ||
implementation, but it's fairly involved. | ||
|
||
## Hooks | ||
|
||
An HTMLBars environment exposes a series of hooks that a runtime | ||
environment can use to define the behavior of templates. These hooks | ||
are defined on the `env` passed into an HTMLBars `render` function, | ||
and are invoked by HTMLBars as the template's dynamic portions are | ||
reached. | ||
|
||
### The Scope Hooks | ||
|
||
Scope management: | ||
|
||
* `createFreshScope`: create a new, top-level scope. The default | ||
implementation of this hook creates a new scope with a `self` slot | ||
for the dynamic context and `locals`, a dictionary of local | ||
variables. | ||
* `createShadowScope`: create a new scope for a template that is | ||
being rendered in the middle of the render tree with a new, | ||
top-level scope (a "shadow root"). | ||
* `createChildScope`: create a new scope that inherits from the parent | ||
scope. The child scope must reflect updates to `self` or `locals` on | ||
the parent scope automatically, so the default implementation of this | ||
hook uses `Object.create` on both the scope object and the locals. | ||
* `bindSelf`: a fresh `self` value has been provided for the scope | ||
* `bindLocal`: a specific local variable has been provided for | ||
the scope (through block arguments). | ||
|
||
Scope lookup: | ||
|
||
* `getRoot`: get the reference for the first identifier in a path. By | ||
default, this first looks in `locals`, and then looks in `self`. | ||
* `getChild`: gets the reference for subsequent identifiers in a path. | ||
* `getValue`: get the JavaScript value from the reference provided | ||
by the final call to `getChild`. Ember.js uses this series of | ||
hooks to create stable streams for each reference that remain | ||
stable across renders. | ||
|
||
> All hooks other than `getValue` operate in terms of "references", | ||
> which are internal values that can be evaluated in order to get a | ||
> value that is suitable for use in user hooks. The default | ||
> implementation simply uses JavaScript values, making the | ||
> "references" simple pass-throughs. Ember.js uses stable "stream" | ||
> objects for references, and evaluates them on an as-needed basis. | ||
|
||
### The Helper Hooks | ||
|
||
* `hasHelper`: does a helper exist for this name? | ||
* `lookupHelper`: provide a helper function for a given name | ||
|
||
### The Expression Hooks | ||
|
||
* `concat`: takes an array of references and returns a reference | ||
representing the result of concatenating them. | ||
* `subexpr`: takes a helper name, a list of positional parameters | ||
and a hash of named parameters (as references), and returns a | ||
reference that, when evaluated, produces the result of invoking the | ||
helper with those *evaluated* positional and named parameters. | ||
|
||
User helpers simply take positional and named parameters and return the | ||
result of doing some computation. They are intended to be "pure" | ||
functions, and are not provided with any other environment information, | ||
nor the DOM being built. As a result, they satisfy the idempotence | ||
requirement. | ||
|
||
Simple example: | ||
|
||
```hbs | ||
<p>{{upcase (format-person person)}}</p> | ||
``` | ||
|
||
```js | ||
helpers.upcase = function(params) { | ||
return params[0].toUpperCase(); | ||
}; | ||
|
||
helpers['format-person'] = function(params) { | ||
return person.salutation + '. ' + person.first + ' ' + person.last; | ||
}; | ||
``` | ||
|
||
The first time this template is rendered, the `subexpr` hook is invoked | ||
once for the `format-person` helper, and its result is provided to the | ||
`upcase` helper. The result of the `upcase` helper is then inserted into | ||
the DOM. | ||
|
||
The second time the template is rendered, the same hooks are called. | ||
HTMLBars compares the result value with the last value inserted into the | ||
DOM, and if they are the same, does nothing. | ||
|
||
Because HTMLBars is responsible for updating the DOM, and simply | ||
delegates to "pure helpers" to calculate the values to insert, it can | ||
guarantee idempotence. | ||
|
||
## Keywords | ||
|
||
HTMLBars allows a host environment to define *keywords*, which receive | ||
the full set of environment information (such as the current scope and a | ||
reference to the runtime) as well as all parameters as unevaluated | ||
references. | ||
|
||
Keywords can be used to implement low-level behaviors that control the | ||
DOM being built, but with great power comes with great responsibility. | ||
Since a keyword has the ability to influence the ambient environment and | ||
the DOM, it must maintain the idempotence invariant. | ||
|
||
To repeat, the idempotence requirement says that if a given template is | ||
executed multiple times with the same dynamic environment, it produces | ||
the same DOM. This means the exact same DOM nodes, with the same | ||
internal state. | ||
|
||
This is also true for all child templates. Consider this template: | ||
|
||
```hbs | ||
<h1>{{title}}</h1> | ||
|
||
{{#if subtitle}} | ||
<h2>{{subtitle}}</h2> | ||
{{/if}} | ||
|
||
<div>{{{body}}}</div> | ||
``` | ||
|
||
If this template is rendered first with a `self` that has a title, | ||
subtitle and body, and then rendered again with the same title and body | ||
but no subtitle, the second render will produce the same `<h1>` and same | ||
`<div>`, even though a part of the environment changes. | ||
|
||
The general goal is that for a given keyword, if all of the inputs to | ||
the keyword have stayed the same, the produced DOM will stay the same. | ||
|
||
## Lifecycle Example | ||
|
||
To implement an idempotent keyword, you need to understand the basic | ||
lifecycle of a render node. | ||
|
||
Consider this template: | ||
|
||
```js | ||
{{#if subtitle}} | ||
<h2>{{subtitle}}</h2> | ||
{{/if}} | ||
``` | ||
|
||
The first time this template is rendered, the `{{#if}}` block receives a | ||
fresh, empty render node. | ||
|
||
It evaluates `subtitle`, and if the value is truthy, yields to the | ||
block. HTMLBars creates the static parts of the template (the `<h2>`) | ||
and inserts them into the DOM). | ||
|
||
When it descends into the block, it creates a fresh, empty render node | ||
and evaluates `subtitle`. It then sets the value of the render node to | ||
the evaluated value. | ||
|
||
The second time the template is rendered, the `{{#if}}` block receives | ||
the same render node again. | ||
|
||
It evaluates `subtitle`, and if the value is truthy, yields to the | ||
block. HTMLBars sees that the same block as last time was yielded, and | ||
**does not** replace the static portions of the block. | ||
|
||
(If the value is falsy, it does not yield to the block. HTMLBars sees | ||
that the block was not yielded to, and prunes the DOM produced last | ||
time, and does not descend.) | ||
|
||
It descends into the previous block, and repeats the process. It fetches | ||
the previous render node, instead of creating a fresh one, and evaluates | ||
`subtitle`. | ||
|
||
If the value of `subtitle` is the same as the last value of `subtitle`, | ||
nothing happens. If the value of `subtitle` has changed, the render node | ||
is updated with the new value. | ||
|
||
This example shows how HTMLBars itself guarantees idempotence. The | ||
easiest way for a keyword to satisfy these requirements are to implement | ||
a series of functions, as the next section will describe. | ||
|
||
## Lifecycle More Precisely | ||
|
||
```js | ||
export default { | ||
willRender: function(node, env) { | ||
// This function is always invoked before any other hooks, | ||
// giving the keyword an opportunity to coordinate with | ||
// the external environment regardless of whether this is | ||
// the first or subsequent render, and regardless of | ||
// stability. | ||
}, | ||
|
||
setupState: function(state, env, scope, params, hash) { | ||
// This function is invoked before `isStable` so that it can update any | ||
// internal state based on external changes. | ||
}, | ||
|
||
isEmpty: function(state, env, scope, params, hash) { | ||
// If `isStable` returns false, or this is the first render, | ||
// this function can return true to indicate that the morph | ||
// should be empty (and `render` should not be called). | ||
} | ||
|
||
isPaused: function(state, env, scope, params, hash) { | ||
// This function is invoked on renders after the first render; if | ||
// it returns true, the entire subtree is assumed valid, and dirty | ||
// checking does not continue. This is useful during animations, | ||
// and in some cases, as a performance optimization. | ||
}, | ||
|
||
isStable: function(state, env, scope, params, hash) { | ||
// This function is invoked after the first render; it checks to see | ||
// whether the node is "stable". If the node is unstable, its | ||
// existing content will be removed and the `render` function is | ||
// called again to produce new values. | ||
}, | ||
|
||
rerender: function(morph, env, scope, params, hash, template, inverse | ||
visitor) { | ||
// This function is invoked if the `isStable` check returns true. | ||
// Occasionally, you may have a bit of work to do when a node is | ||
// stable even though you aren't tearing it down. | ||
}, | ||
|
||
render: function(node, env, scope, params, hash, template, inverse, visitor) { | ||
// This function is invoked on the first render, and any time the | ||
// isStable function returns false. | ||
} | ||
} | ||
``` | ||
|
||
For any given render, a keyword can end up in one of these states: | ||
|
||
* **initial**: this is the first render for a given render node | ||
* **stable**: the DOM subtree represented by the render node do not | ||
need to change; continue revalidating child nodes | ||
* **unstable**: the DOM subtree represented by the render node is no | ||
longer valid; do a new initial render and replace the subtree | ||
* **prune**: remove the DOM subtree represented by the render node | ||
* **paused**: do not make any changes to this node or the DOM subtree | ||
|
||
It is the keyword's responsibility to ensure that a node whose direct | ||
inputs have not changed remains **stable**. This does not mean that no | ||
descendant node will not be replaced, but only the precise nodes that | ||
have changed will be updated. | ||
|
||
Note that these details should generally **not** be exposed to the user | ||
code that interacts with the keyword. Instead, the user code should | ||
generally take in inputs and produce outputs, and the keyword should use | ||
those outputs to determine whether the associated render node is stable | ||
or not. | ||
|
||
Ember `{{outlet}}`s are a good example of this. The internal | ||
implementation of `{{outlet}}` is careful to avoid replacing any nodes | ||
if the current route has not changed, but the user thinks in terms of | ||
transitioning to a new route and rendering anew. | ||
|
||
If the transition was to the same page (with a different model, say), | ||
the `{{outlet}}` keyword will make sure to consider the render node | ||
stable. | ||
|
||
From the user's perspective, the transition always results in a complete | ||
re-render, but the keyword is responsible for maintaining the | ||
idempotence invariant when appropriate. | ||
|
||
This also means that it's possible to precisely describe what | ||
idempotence guarantees exist. HTMLBars defines the guarantees for | ||
built-in constructs (including invoked user helpers), and each keyword | ||
defines the guarantees for the keyword. Since those are the only | ||
constructs that can directly manipulate the lexical environment or the | ||
DOM, that's all you need to know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing trailing coma?