|
| 1 | +# Autotracked Rendering |
| 2 | + |
| 3 | +An explanation of the depths of the reactivity system we've been using and refining since Ember Octane (ember-source 3.13+). |
| 4 | + |
| 5 | +### Walkthrough: setting a value |
| 6 | + |
| 7 | +Given: |
| 8 | +```gjs |
| 9 | +import { tracked } from '@glimmer/tracking'; |
| 10 | +
|
| 11 | +class ModuleState { |
| 12 | + @tracked count = 0; |
| 13 | +
|
| 14 | + increment() { |
| 15 | + this.count++; |
| 16 | + } |
| 17 | +} |
| 18 | +
|
| 19 | +const state = new ModuleState(); |
| 20 | +
|
| 21 | +<template> |
| 22 | + <output>{{ state.count }}</output> |
| 23 | + <button {{on "click" state.increment}}>Increment</button> |
| 24 | +</template> |
| 25 | +``` |
| 26 | + |
| 27 | +And we |
| 28 | +1. observe a render, |
| 29 | +2. and then click the button, |
| 30 | + - and then observe the output count update. |
| 31 | + |
| 32 | +How does it work? |
| 33 | + |
| 34 | +There are a few systems at play for autotracking: |
| 35 | +- [tags][^vm-tags] |
| 36 | +- [global context][^ember-global-context] |
| 37 | +- the environment / delegate |
| 38 | +- some [glue code][^ember-renderer] that [configures][^ember-renderer-revalidate] the [timing specifics][^ember-renderer-render-transaction] of when to [render updates][^ember-renderer-render-roots] |
| 39 | +- the actual [call to the VM to render][^ember-root-state-render] |
| 40 | + |
| 41 | + |
| 42 | +[^vm-tags]: https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/validator/lib/validators.ts#L1 |
| 43 | +[^ember-global-context]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/environment.ts#L21 |
| 44 | +[^ember-renderer]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L613C1-L614C1 |
| 45 | +[^ember-renderer-revalidate]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L626 |
| 46 | +[^ember-renderer-render-transaction]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L573 |
| 47 | +[^ember-renderer-render-roots]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L524 |
| 48 | +[^ember-root-state-render]: https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L156 |
| 49 | + |
| 50 | +#### 1. leading up to observing a render |
| 51 | + |
| 52 | +- **render** |
| 53 | + - call `renderMain()` from glimmer-vm |
| 54 | + - this creates a [VM instance and a TemplateIterator](https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/runtime/lib/render.ts#L59) |
| 55 | + - tell the [renderer to render](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/glimmer/lib/renderer.ts#L165-L168) |
| 56 | + 1. [executes the VM](https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/runtime/lib/render.ts#L32) |
| 57 | + 2. iterates over blocks / defers to [_execute](https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/runtime/lib/vm/append.ts#L728) |
| 58 | + 3. [evaluate opcodes](https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/runtime/lib/vm/append.ts#L770) |
| 59 | + 4. this brings us to the [low-level VM](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/runtime/lib/vm/low-level.ts#L167) |
| 60 | + 5. the low-level VM is the actual VirtualMachine which inteprets all our opcodes -- it iterates until there are no more opcodes |
| 61 | + |
| 62 | +- **read: count** |
| 63 | + - access `count`, which `@tracked`'s getter [defers to `trackedData`](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/metal/lib/tracked.ts#L155C28-L155C39) |
| 64 | + - the [`trackedData`](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/metal/lib/tracked.ts#L5) is in `@glimmer/validator` instead of using tags _directly_. |
| 65 | + - `trackedData` calls `consumeTag` when [the value is access](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/validator/lib/tracked-data.ts#L15) |
| 66 | + - `consumeTag` adds the tag to the [`CURRENT_TRACKER`](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/validator/lib/tracking.ts#L116) |
| 67 | + - this is so that when any `{{ }}` regions of a template "detect" a dirty tag, they can individually re-render |
| 68 | + |
| 69 | + |
| 70 | + - [valueForRef](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/reference/lib/reference.ts#L155) |
| 71 | + - called by _many_ opcode handlers in the VM, in this case: [this APPEND_OPCODE](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/runtime/lib/compiled/opcodes/content.ts#L88) |
| 72 | + - [track](https://github.com/glimmerjs/glimmer-vm/blob/main/packages/%40glimmer/validator/lib/tracking.ts#L232) |
| 73 | + - calls [beginTrackFrame](https://github.com/glimmerjs/glimmer-vm/blob/d86274816a21c61fbc82059006fe7687ca17dc7e/packages/%40glimmer/validator/lib/tracking.ts#L58) and the corresponding `endTrackFrame()` |
| 74 | + |
| 75 | +- **render a button with modifier** |
| 76 | + - for demonstration purposes, this phase is skipped in this explanation, as this document is more about auto-tracking, and less so about how elements and event listeners get wired up |
| 77 | + |
| 78 | +#### 2. click the button |
| 79 | + |
| 80 | +- `increment()` |
| 81 | + - **read: count** |
| 82 | + - reading is part of `variable++` behavior |
| 83 | + - **set: count** |
| 84 | + - we dirty the tag [via `@tracked`'s setter](https://github.com/emberjs/ember.js/blob/132b66a768a9cabd461908682ef331f35637d5e9/packages/%40ember/-internals/metal/lib/tracked.ts#L171) |
| 85 | + |
| 86 | + - `scheduleRevalidate()` is called by `dirtyTag()`, which then defers to ember to call these things and interacts with the scheduler (we go back to step 1): |
| 87 | + - **env.begin** |
| 88 | + - **env.rerender** |
| 89 | + - **read: count** |
| 90 | + - **env.commit** |
| 91 | + |
| 92 | +the output, `count` is rendered as `1` |
| 93 | + |
| 94 | + |
| 95 | +### A minimal renderer |
| 96 | + |
| 97 | +[JSBin, here](https://jsbin.com/mobupuh/edit?html,output) |
| 98 | + |
| 99 | +> [!CAUTION] |
| 100 | +> This is heavy in boilerplate, and mostly private API. This 300 line *minimal* example, should be considered our todo list, as having all this required to render a tiny component is _too much_. |
| 101 | +
|
| 102 | + |
0 commit comments