-
Notifications
You must be signed in to change notification settings - Fork 140
Conversation
The implementation in this PR is extremely WIP, so don't let the implementation scare you 😉 |
looks amazing! |
95e6357
to
26541a0
Compare
One question about how the diff-ing works. That would be just beautiful. |
@cibernox Others can correct me if I am wrong, but this work does not make any effort toward that aim. In fact, most libraries the perform dom-diffing are performing dom-like-data-tree diffing. The "virtual DOM" as we call it. They don't actually parse HTML into DOM, they require you to use a DSL to write dom-like objects that are later translated into DOM. Since a value inside a Additionally, the case of Hope that makes some sense! |
or just old{{{}}} === new{{{}}} :P |
I imagined that. I had a very particular use-case where some cards contain safe HTML coming directly from a CMS. Although I can imagine a workaround. Converting the HTML to templates in runtime (I would have to include the HTMLBars compiler) and render views dinamically instead of using {{{}}}. Sounds possible in my head :P |
@@ -1,6 +1,7 @@ | |||
/*jshint evil:true*/ | |||
import { preprocess } from "../htmlbars-syntax/parser"; | |||
import TemplateCompiler from "./template-compiler"; | |||
import { wrap } from "../htmlbars-runtime/hooks"; |
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.
shouldn't be importing from htmlbars-runtime
from within htmlbars-compiler
for anything other than tests (the runtime is not available within Ember for example). If the wrap function needs to be shared and available to compiler (and ultimately to the consuming library) it should be in htmlbars-utils
.
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.
Nice. If utils
is imported into both runtime
and compiler
, that's the right place for this for sure.
fd345f7
to
13257d7
Compare
Previously, if you wanted to re-render an HTMLBars template, the template would create a brand new clone of the cached fragment and create a new set of morphs. This resulted in unnecessary work, since the static portions of the rendered DOM already exist, and do not need to be re-cloned and re-inserted into DOM. This change begins the process of allowing an HTMLBars template to update the output of a previous render, rather than starting from scratch. It introduces the concept of a (very WIP atm) “result” of a previous render, which is passed back in to subsequent renders. At the moment, the “result” is the fragment and morphs of the previous render. Soon, it will become a more opaque data structure that includes the results of sub-programs.
This commit introduces the notion of a “result” data type, which encapsulates all of the state needed to be passed to a compiled template in order to successfully re-render with an existing element. Rather than returning a document fragment, `render()` now returns this result type. For compatibility, consumers of this API will need to use the `fragment` property of the result rather than the result directly. In order to re-render, you can pass the last result as the `lastResult` option, and it will use that element and morph to update. One small additional change: contextual elements must now be passed in the options hash. We decided to make this change as the list of arguments was growing unwieldy, and we wanted to make this durable to changes in the future. We will also investigate incorporating block params into the options hash.
This change is an important step towards our efforts to offer rerendering of an existing element, rather than creating a new document fragment each time a template’s context changes. In the old compiled templates, the monolithic render method did all of the following: 1. Decide whether to use a cached fragment or build a new one from the build method. 2. Clone the fragment if possible / necessary 3. Create variables pointing at the fragment’s children (element0, etc.) 4. Create a bunch of pointers (morphs) with those element variables 5. Fill in the pointers (“populate”) Because all of the “pointers” necessary to update the DOM in the future were created as variables in the function scope, access to them was lost as soon as the function exited. This change is designed to make it possible to run step (4) above over and over again on the same piece of DOM. In order to make that possible, we changed the first few steps so that the pointers into the fragment are returned as an array, where they can be passed into a standalone “populate” function. For future readers, the “pointers” into the DOM are now called “render nodes”. These render nodes can point at either elements, attributes, or ranges. Expect these nodes to gain more responsibilities in the future. We also extracted the cached fragment logic into a hook, rather than repeating the completely static content in every template, significantly shrinking the size of compiled templates. The next step is to break up the `render` method into multiple pieces, so that the part that fills in the render nodes is completely standalone, and can be called by passing in the previous render nodes. (We wrote down our original design that we are still more-or-less iterating towards at https://gist.github.com/wycats/fc53be32abee6c5ca0f4.)
This commit finishes the process of breaking apart the template into discrete phases: 1. Building the static DOM “skeleton” 2. Creating morphs (render nodes) that point into the dynamic portions of the DOM skeleton 3. Filling in the dynamic portions with content from the context object By breaking these into discrete steps, we can extract the code that coordinates the steps into library code, significantly reducing the size of compiled templates. This also finally unlocks a very nice API for rerendering a template that reuses the previously generated DOM, rather than starting from a new “skeleton” each time. Not only is updating an existing DOM much less expensive, it allows us to rerender large portions of an application’s UI without losing state like cursor position or scroll position. This technique was pioneered by React. Calling `render()` on a template returns a new object that has both a `fragment` property on it as well as a `rerender()` method. To use re-rendering, call `render(ctx, env, options)` on a template, then save the result. You can put the fragment into the DOM, and when you want to update the template, simply call the `rerender()` method, passing the appropriate context. ```js var result = template.render(model, env, options); document.appendChild(result.fragment); // … later result.rerender(newModel); ```
Previously, invoking a block helper would always return a string or element that would be inserted into the parent fragment. This model assumed that the root-most template was always being rendered exactly once. With the introduction of re-rendering, block helpers need to handle this case as well. Specifically, instead of just creating rendering a new element each time, they should populate a provided morph with the contents they want to display. As an optimization, you can also save off the result of the previous render and `rerender()` it. This change is necessary to support re-rendering, but we consider it a low-level API. We intended to create a high-level API for authors to use that handles the most frequent rendering and re-rendering patterns. In particular, the test titled “Templates with block helpers - re-rendering” shows a low-level pattern that can be used to support efficient re-rendering in general, and which should be easy to extract into something higher level once we have made sure we have handled all of the necessary cases.
Previously, invoking a child template in a block helper looked like this: ```js registerHelper(‘identity’, function(params, hash, options, env) { return options.template.render(this, env, options, blockParams); }); ``` It now looks like this: ```js registerHelper(‘identity’, function(params, hash, options, env) { return options.template.render(this, blockParams); }); ```
In the future, frameworks like Ember that implement the `get` hook need some way to mark a render node as dirty. To that effect, we need to provide the `get` helper with a reference to its associated render node, such that if the underlying value changes, it can mark the node as dirty.
Previously, we would always revalidate all render nodes in a tree when calling `revalidate()` on the root render node. As a performance optimization, we introduce the notion of dirtiness to render nodes. We have updated all of the built-in hooks that modify render nodes to first check to see if the node is dirty. If the node is not dirty, we quickly bail out. When the render node related to a block helper (or component) is not dirty, it simply asks its child nodes to continue dirty checking. Just because a block helper or component is dirty does not necessarily mean that it needs to be replaced. For example, an `#if` helper whose content changes may not flip from the truthy template to the falsy template. For this reason, helpers can choose to return the result of rendering a new template, or communicate that the existing template is still stable. If a template is stable, HTMLBars treats it as if the node was not dirty to begin with, and continues dirty checking its children. To support this effort, all of the built-in hooks (content, block, subexpr, etc) now take a render node. These hooks can quickly check the render node for dirtiness and abandon any expensive work if it is false. The intent of this API is for consumers like Ember.js to narrowly target nodes for dirtying when using observers or streams, or dirty entire subtrees when triggered manually by users. Whenever a node becomes dirty, it would schedule a revalidation of the root render node. This guarantees that each node is checked only once, no matter how many changes occur in the tree during a single run loop. It also would guarantee that parent nodes have a chance to re-render themselves before any changes affecting their old children are processed.
This commit makes all tests pass with the new visitor structure, significantly shrinking the size of compiled templates and making it possible to do re-renders without having to execute a render function. This also means that, in theory, hooks like `inline` can know the structure of what they're invoking before actually invoking it, allowing implementation to do smarter caching at the "statement" level. This change is honestly mostly a wonky improvement, but it does help us avoid making as many decisions in the compiled template, deferring them to the runtime. There is still a decent amount of cruft left as of this commit, which we will remove momentarily.
Now that the template is largely just a data structure that describes its statements and expressions, the `render` function is no longer necessary. Instead, the runtime `render` function uses the data structure to decide what to do. At the moment, it simply delegates to the hooks that were already there, but the plan is to allow runtimes to completely swap out the expression visitor to do more exotic things like building and caching streams.
Previously, if a template’s first (or last) child was a dynamic node (like a mustache or block), the associated render node’s firstChild or lastChild would change. ```hbs {{#if condition}}{{value}}{{/if}} ``` In this case, the render node associated with the `#if` helper cannot have stable `firstNode` and `lastNode`, because when the nodes associated with `value` change, the nodes associated with the `if` helper must change as well. The solution was to require that templates create fragments with stable start and end nodes. If the first node is dynamic, we insert a boundary empty text node to ensure that the template’s boundaries will not change.
This commit refactors the internal scope handling of templates so that the concept of a scope, including block parameters, is rigorous. It also stops leaking the caller's scope into a helper call. This commit has two primary changes: * The internal "scope" is now broken up into `self` and `locals`. Block helpers now receive `options.template.yield`, which is used like `options.template.yield([ optional, block, params ])`. If block parameters are supplied, HTMLBars will ask the host to create a new scope frame, and the `locals` hash will be populated with the yielded parameters. It is still possible to shift the `self` by using `options.template.render(newSelf, [ optional, block, params ])`, which will always create a new scope frame. Since self-shifting helpers are intended to be rare, `yield` is the new path, and encapsulates all of the scoping details. * Helpers and block helpers no longer receive the `self` as `this`, because they are intended to be analogous to functions. Functions do not have access to the scope of the caller O_o. Note that hooks, which are used by the host to implement the scoping semantics, of course still have access to the scope.
769af95
to
5c06422
Compare
This commit ensures that when any of `morph`, `env`, `scope`, `options`, or `blockArguments` exist in a function signature, that they appear in a consistent order (namely the one we used above). This commit also ends the practice of passing `env` to helpers, because helpers should be seen as functions operating within the scope system of HTMLBars, not arbitrary escape valves that can contort and abuse the template scope. Instead, environments implement the scoping system (and any keywords) by customizing the hooks invoked by template rendering. As of this commit, `partial` is a hook-defined keyword. In Ember `yield`, which also needs access to ambient scope information, will also be a hook-defined keyword. This finishes a separation between the hooks, which are used by environments to define the "programming language" of HTMLBars, and helpes, which are user-provided functions that operate within the scoping rules of HTMLBars.
```js registerHelper('random', function() { if (Math.random() > 0.5) { return this.yield(); } }); ``` This allows you to write a helper that takes a block and easily invokes it. The `yield` method can take block parameters: ```hbs {{#count as |i|}} <p>Rendered {{i}} times</p> {{/count}} ``` ```js var count = 0; registerHelper('count', function() { this.yield([ ++count ]); }); ``` Behind the scenes, this method passes along the entire current scope as well as the render node being filled in and the contextual element.
The RenderNode already has a reference to its contextual element, so passing extracting it from the render node is unnecessary.
Continued in #318. |
This is a WIP PR to implement React-inspired fast rerendering of HTMLBars templates.
One important improvement we can make over the React model is eliminating any work for static portions of the template. Because HTMLBars templates are declarative, not imperative like React's model, the compiled template knows which parts of the DOM can change (because they were created through
{{}}
s) and which parts will never change.Compiled HTMLBars templates already separate out the process of building the static "skeleton" from the process of filling it in, and this PR allows us to update the dynamic portions of the result of a previous render. Conceptually, this allows us to run the same "rendering" code multiple times, making re-renders fast without having to expensively diff parts of the DOM that will never change.
Because conditionals and loops are also declarative, we can eliminate the cognitive overhead React requires when conditionally rendering content. Instead of requiring that the user remember to insert
null
into a conditional slot, the template engine will know the position of an{{#if}}
block, regardless of whether the conditional is truthy or not. There is no way for the user to screw this up and produce an off-by-one misalignment, which circumvents the diffing algorithm and triggers a full re-render.TODO
Make inline helpers render nodes{{yield}}
for componentsoptions.render
)rootRenderNode
through the render node hierarchyutils