Improving the render cycle API
johan-gorter opened this issue · comments
You can write your whole application without doing anything with the render cycle. This is part of our philosophy of keeping things pure and simple. But you do need the render cycle if:
- You integrate components/widgets that are not written using maquette.
- Do advanced animations.
- Interact with the DOM API.
- Measure nodes.
Every time a new screen is rendered, the following phases take place:
- Render render() functions are called
- Diff and patch The real DOM is updated
- Measure The phase for callbacks that need to take measurement of the layouted DOM (getBoundingClientRect)
- WrapUp Phase in which the DOM may be modified again
It is important to have a separate measure and wrapUp phase, because if multiple components were to measure and change the DOM in the same phase, unneeded reflows take place which would hurt performance.
Every time a render takes place, a new RenderRun
object is created. All callbacks that are called during a render get a reference to the RenderRun
object as a parameter. The RenderRun
has the following interface:
Interface RenderRun {
duringMeasure(callback: () => void): void;
duringWrapUp(callback: () => void): void;
}
The RenderRun can then be used as follows:
h('div', { enterAnimation: growToAutoHeight })
let growToAutoHeight = (element: Element, run: RenderRun) => {
let autoHeight = 0;
run.duringMeasure(() => {
autoHeight = element.getBoundingClientRect().height;
});
run.duringWrapUp(() => {
element.style.height = '0px';
let grow = element.animate([{height: '0px', height: autoHeight+'px'}]);
grow.onfinish(() => {element.style.height = ''})
})
}
The following phases execute when a render takes place:
- render() functions are executed.
- Diffing and patching of the real DOM. For each DOM node that is processed, the following happens:
- afterFirstRender callback is called if the DOM node is new
- afterRender callback is called
- enterAnimation is called if the DOM node is new and its parent already existed
- updateAnimation is called if DOM node is updated
- The DOM node is attached to the parent DOM node if DOM node is new
- exitAnimation (from previous render) is called if DOM node is removed and its parent remains
- Callbacks on RenderRun.duringMeasure are executed
duringMeasure callbacks may be used to measure the DOM (getBoundingClientRect), but may not change the DOM. - Callbacks on RenderRun.duringWrapUp are executed
duringWrapUp callbacks may change the DOM, but should not measure the DOM.
When all code uses the RenderRun object appropriately, all updates to the UI should never cause more than 2 reflows.
Migration path from afterCreate and afterUpdate:
AfterCreate can be replaced with afterFirstRender
. Note that during afterFirstRender
, the DOM Node is not attached to its parent. If the afterCreate
code needed this, afterFirstRender
can register code to run during measure or wrapUp.
AfterUpdate can be replaced with afterRender
. Note that afterRender
also runs when the DOM node is first created and at that time it will not have a parent Node.
This is one of our ideas for maquette 3
@johan-gorter For the WrapUp
phase, is the idea to only modify the DOM directly (i.e., no VDOM)? For example, if during Measure
I know that I need to add/remove a CSS class, would I do it directly on the element or would VNode#class
come into play?
@jcfranco The VNode should not be modified, once it is passed to maquette. Maquette keeps a reference to this VNode to do a diff during the next run.
The wrapUp phase should only modify the real DOM. The data that is used to render the VNode-tree may be modified during measure or wrapUp.
For example: if a UI Component renders itself it may leave a duringMeasure
callback to measure if it fits on the screen properly. If it does not fit on the screen, it may leave a duringWrapUp
callback to add a compact
css class to the component.
But if the component needs a totally different UI in compact mode, the duringMeasure
may instead change an internal compact
variable and trigger a scheduleRender
, so the render function is executed again.
@johan-gorter That makes sense, thanks for the explanation.
We decided it would benefit everyone if we finish the other 3.0 issues first, so everyone can profit from these and to delay this issue bit, because this has more impact on codebases.