@lukekaalim/act-recon 3.0.0-alpha.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/algorithms.ts +55 -41
- package/commit.ts +103 -60
- package/delta.ts +71 -41
- package/docs/lifecycle_of_an_update.md +465 -0
- package/element.ts +153 -143
- package/hooks.ts +49 -32
- package/mod.ts +9 -7
- package/package.json +2 -2
- package/pool.ts +48 -0
- package/readme.md +52 -2
- package/reconciler.ts +83 -108
- package/state.ts +82 -9
- package/thread.ts +256 -260
- package/tree.ts +196 -54
- package/update.ts +48 -109
- package/context.ts +0 -19
- package/event.ts +0 -30
- package/work.ts +0 -10
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# Lifecycle of an Update
|
|
2
|
+
|
|
3
|
+
Journey with us as we go through the code to see which
|
|
4
|
+
systems are responsible for the various parts of
|
|
5
|
+
managing an Act Tree inside `@lukekaalim/act-recon`.
|
|
6
|
+
|
|
7
|
+
We'll start at a button onclick handler, and end up
|
|
8
|
+
where we submit a packet of changes to our renderer.
|
|
9
|
+
|
|
10
|
+
> This description of these processes is a little simplified to skip some of the
|
|
11
|
+
> more complicated edge cases - so example code here does not 1-1 match
|
|
12
|
+
> the actual library code.
|
|
13
|
+
|
|
14
|
+
1. [Clicking the Button](#1-clicking-the-button)
|
|
15
|
+
2. [Inside the Reconciler](#2-inside-the-reconciler)
|
|
16
|
+
3. [Queuing a new WorkTask](#3-queuing-a-new-work-task)
|
|
17
|
+
4. [Working in the Thread](#4-working-in-the-thread)
|
|
18
|
+
5. [Learning the History](#5-learning-the-history)
|
|
19
|
+
6. [Running the Component](#6-running-the-component)
|
|
20
|
+
7. [Generating the Diff](#7-generating-the-diff)
|
|
21
|
+
8. [Handling the Output](#8-handling-the-output)
|
|
22
|
+
9. [Submitting the Delta](#9-submitting-the-delta)
|
|
23
|
+
10. [Conclusion](#10-conclusion)
|
|
24
|
+
|
|
25
|
+
## 1. Clicking the Button
|
|
26
|
+
|
|
27
|
+
Typically, a user will interact with an application in some way - click a button,
|
|
28
|
+
type some text. Something like:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const [count, setCount] = useState(0)
|
|
32
|
+
|
|
33
|
+
function onClick() {
|
|
34
|
+
setCount(count + 1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return h('button', { onClick }, `Clicked ${count} times!`);
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Our "setCount" call is the function that starts everything off! Otherwise
|
|
41
|
+
referred to as the `setState` function.
|
|
42
|
+
|
|
43
|
+
The setState function generated by `useState()` has captured a few variables
|
|
44
|
+
internally when it was created:
|
|
45
|
+
- A ["CommitRef2"](/Reconciler#@lukekaalim/act-recon.CommitRef2) to a specific
|
|
46
|
+
part of our Act Tree where the calling component lives.
|
|
47
|
+
- A "ComponentState" - a mutable instance that stores all the variables relevant
|
|
48
|
+
for rendering a component, including such things as a map of
|
|
49
|
+
literal values of each `useState` call.
|
|
50
|
+
- The ["Reconciler"](/Reconciler#@lukekaalim/act-recon.Reconciler2) to which this entire app belongs.
|
|
51
|
+
|
|
52
|
+
So, to kick off our update, we need to two things: we need to update the internal
|
|
53
|
+
state of our component to use the new value provided to setState, and we need
|
|
54
|
+
to re-render the component. For the second one, we just ask the reconciler, and give it
|
|
55
|
+
that `ref` we had from before
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// somewhere
|
|
60
|
+
const ref: CommitRef2 = ...;
|
|
61
|
+
const componentState: ComponentState = ...;
|
|
62
|
+
const reconciler: Reconciler2 = ...;
|
|
63
|
+
|
|
64
|
+
// the second return value from useState
|
|
65
|
+
// e.g.
|
|
66
|
+
// [state, setState] = useState();
|
|
67
|
+
setState(newValue) {
|
|
68
|
+
// update our component state
|
|
69
|
+
componentState.values.set(hookId, newValue);
|
|
70
|
+
// then request our component be re-rendered
|
|
71
|
+
reconciler.render(ref);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 2. Inside the Reconciler
|
|
76
|
+
|
|
77
|
+
Next stop: the reconciler. It's received a request
|
|
78
|
+
to re-render a specific component. It's mainly going
|
|
79
|
+
to delegate to it's [WorkThread](/Reconciler#@lukekaalim/act-recon.WorkThread2),
|
|
80
|
+
but while it does it will also tell the **Scheduler** that it should
|
|
81
|
+
get itself ready to do some work.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// somewhere
|
|
85
|
+
const scheduler: Scheduler = ...;
|
|
86
|
+
const thread: WorkThread2 = ...;
|
|
87
|
+
|
|
88
|
+
render(ref: CommitRef2) {
|
|
89
|
+
thread.queue(ref);
|
|
90
|
+
scheduler.requestCallback();
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 3. Queuing a new WorkTask
|
|
95
|
+
|
|
96
|
+
Inside the WorkThread, it does a few checks before actually
|
|
97
|
+
promising to do any work:
|
|
98
|
+
- Am I already in the middle of a render? If so:
|
|
99
|
+
- Do I already have an update for this commit specifically?
|
|
100
|
+
If so, I don't actually need to do anything.
|
|
101
|
+
- Have I already rendered this Commit? I'm only allowed
|
|
102
|
+
to render a commit once per "pass", so I'll add this to my
|
|
103
|
+
"missed" queue, and I'll get started on it next pass.
|
|
104
|
+
- Am I currently rendering any of this commit's parents?
|
|
105
|
+
If so, just mark all the bits in-between as "MustVisit",
|
|
106
|
+
and that other update will handle the re-render.
|
|
107
|
+
|
|
108
|
+
If none of those conditions are met, the thread will add a
|
|
109
|
+
new [WorkTask](/Reconciler#@lukekaalim/act-recon.WorkTask) to it's queue.
|
|
110
|
+
|
|
111
|
+
To add this work task, we need to turn the CommitRef into the
|
|
112
|
+
actual commit it references - we can do this by just asking
|
|
113
|
+
the commit tree (which the WorkThread has access to)! We can
|
|
114
|
+
just pass the commit to once of the WorkTask constructors
|
|
115
|
+
to tell it what kind of operation we want to perform.
|
|
116
|
+
|
|
117
|
+
In this case, all we want is for the task to visit that node,
|
|
118
|
+
which should cause a re-render (thus updating our component).
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const tree: CommitTree2 = ...;
|
|
122
|
+
|
|
123
|
+
const missed = new Set<CommitID>();
|
|
124
|
+
const mustVisit = new Set<CommitID>();
|
|
125
|
+
const mustRender = new Set<CommitID>();
|
|
126
|
+
const visited = new Set<CommitID>();
|
|
127
|
+
|
|
128
|
+
const tasks: WorkTask[] = [];
|
|
129
|
+
|
|
130
|
+
queue(ref) {
|
|
131
|
+
if (mustRender.has(ref.id))
|
|
132
|
+
return;
|
|
133
|
+
|
|
134
|
+
if (visited.has(ref.id)) {
|
|
135
|
+
missed.add(ref);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (ref.path.find(ancestorId => tasks.find(task => task.ref.id === ancestorId))) {
|
|
140
|
+
ref.path.forEach(ancestorId => mustVisit.add(ancestorId))
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
mustRender.add(ref.id);
|
|
145
|
+
mustVisit.add(ref.id);
|
|
146
|
+
const commit = tree.commits.get(ref.id) as Commit2;
|
|
147
|
+
tasks.push(WorkTask.visit(commit));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 4. Working in the Thread
|
|
153
|
+
|
|
154
|
+
After adding a new tasks to the queue, nothing immediately happens. But
|
|
155
|
+
remember previously, when we called `scheduler.requestCallback`? At
|
|
156
|
+
some point later, the callback we requested will trigger, sending us back to the
|
|
157
|
+
reconciler.
|
|
158
|
+
|
|
159
|
+
The reconciler asks the thread if there is work to do (the thread checks it's tasks,
|
|
160
|
+
which we might have just added to). If there is, the reconciler asks the thread to work!
|
|
161
|
+
|
|
162
|
+
Kicking things off here, our thread just pops off the first task off it's task queue.
|
|
163
|
+
It does a few more checks to see if it can skip out on doing work, such as:
|
|
164
|
+
- Does this task just ask be to visit this node? If so:
|
|
165
|
+
- Is this node on my MustVisit list? Skip it not.
|
|
166
|
+
- Is this node something I have to directly render? If not
|
|
167
|
+
(aka I need to visit this node, but I don't need to render it),
|
|
168
|
+
just queue up some tasks to visit this node's children.
|
|
169
|
+
|
|
170
|
+
Otherwise, it will commit to doing the work - it's going to start
|
|
171
|
+
by asking the tree to process the commit we have inside the WorkTask.
|
|
172
|
+
|
|
173
|
+
The tree will give us an output object, which we will use later.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const tree: CommitTree2 = ...;
|
|
177
|
+
|
|
178
|
+
work() {
|
|
179
|
+
const task = tasks.pop();
|
|
180
|
+
|
|
181
|
+
if (task.isVisiting()) {
|
|
182
|
+
if (!mustVisit.has(task.ref.id))
|
|
183
|
+
return;
|
|
184
|
+
|
|
185
|
+
if (!mustRender.has(task.ref.id)) {
|
|
186
|
+
const visits = task.commit.children
|
|
187
|
+
.map(child => WorkTask.visit(child));
|
|
188
|
+
tasks.push(visits);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const output = tree.processElement(task.commit.element, task.commit)
|
|
194
|
+
// more later
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 5. Learning the History
|
|
199
|
+
|
|
200
|
+
A "Visit" task is actually just an instance of a generic task where the
|
|
201
|
+
"prev" element and the "next" element are the same - we know that our
|
|
202
|
+
component's special internal state has changed, not it's external props.
|
|
203
|
+
|
|
204
|
+
> Other types of Tasks often describe a specific change - the addition
|
|
205
|
+
> of more props or the changing of a value.
|
|
206
|
+
|
|
207
|
+
When a element is "processed" (like a component or "div"), it should
|
|
208
|
+
return some children, which should create additional tasks to process.
|
|
209
|
+
|
|
210
|
+
We want to make sure that children generated in the past are matched up
|
|
211
|
+
to the return values of the component, so when a component renders a second
|
|
212
|
+
time we can match up it's old children to it's new children.
|
|
213
|
+
|
|
214
|
+
Our tree's processing has a few tasks: first, it checks our commit's
|
|
215
|
+
history to see who our old children were, then figure out what kind
|
|
216
|
+
of element we are. There are some special cases for Providers and Boundaries,
|
|
217
|
+
but the one we care about is the "Component", or
|
|
218
|
+
`typeof element.type === 'function'` case.
|
|
219
|
+
|
|
220
|
+
The tree loads the previous children, and the components current state, before
|
|
221
|
+
passing it to the ElementOutput, telling it to process the component. The tree
|
|
222
|
+
also has a reference to the reconciler, which it passes along too.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const reconciler: Reconciler2 = ...;
|
|
226
|
+
const componentStates: Map<CommitID, ComponentState> = ...;
|
|
227
|
+
|
|
228
|
+
processElement(element: Element, commit: CommitRef2) {
|
|
229
|
+
const output = new ElementOutput2()
|
|
230
|
+
output.prevChildren = commit.children.map(childRef => commits.get(childRef.id));
|
|
231
|
+
|
|
232
|
+
switch (typeof element.type) {
|
|
233
|
+
case 'function':
|
|
234
|
+
const state = componentStates.get(ref.id);
|
|
235
|
+
|
|
236
|
+
output.processComponent(commit.ref, element.type, element, reconciler, state);
|
|
237
|
+
return output;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## 6. Running the Component
|
|
244
|
+
|
|
245
|
+
The ElementOutput is finally responsible for determining all the things
|
|
246
|
+
that will change when the component is processed. First thing it
|
|
247
|
+
does it is sets up the Hook Globals - assigning implementations to
|
|
248
|
+
the hooks imported from `@lukekaalim/act`;
|
|
249
|
+
|
|
250
|
+
> This is actually where,
|
|
251
|
+
> from the first chapter, those variables like ComponentState and Reconciler
|
|
252
|
+
> are captured by our hooks.
|
|
253
|
+
|
|
254
|
+
Once everything is set up, we actually call our component function (finally!).
|
|
255
|
+
|
|
256
|
+
The results of the function (the element actually returned by our component)
|
|
257
|
+
is passed to our calculateDiff function, which uses the history we have from
|
|
258
|
+
our previous children, and our new result, the identify what nodes should
|
|
259
|
+
be created, updated, or destroyed.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
|
|
263
|
+
processComponent(ref, component, element, reconciler, componentState) {
|
|
264
|
+
// make hooks and set them up
|
|
265
|
+
hookImplementation.useState = function useState() {
|
|
266
|
+
const hookId = state.hookIndex++;
|
|
267
|
+
|
|
268
|
+
function setState(newValue) {
|
|
269
|
+
// update our component state
|
|
270
|
+
componentState.values.set(hookId, newValue);
|
|
271
|
+
// then request our component be re-rendered
|
|
272
|
+
reconciler.render(ref);
|
|
273
|
+
}
|
|
274
|
+
// ...blah
|
|
275
|
+
return [value, setState]
|
|
276
|
+
}
|
|
277
|
+
// and do so for useEffect & useContext...
|
|
278
|
+
|
|
279
|
+
// reset the hook counter, used to give
|
|
280
|
+
// each "call" of a hook a unique identifier
|
|
281
|
+
state.hookIndex = 0;
|
|
282
|
+
|
|
283
|
+
// run the component!
|
|
284
|
+
const result = component(element.props);
|
|
285
|
+
|
|
286
|
+
this.diff = this.calculateDiff(result);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
## 7. Generating the Diff
|
|
291
|
+
|
|
292
|
+
We want to essentially learn four things:
|
|
293
|
+
- Stuff was there before but isn't now (Deleted)
|
|
294
|
+
- Stuff wasn't there before but is there now (Created)
|
|
295
|
+
- Stuff that moved around (Moved)
|
|
296
|
+
- Stuff that stayed the same (Persisted)
|
|
297
|
+
|
|
298
|
+
We'll call all these facts a "ChangeReport". We can actually compact
|
|
299
|
+
the representation a little into just two arrays: "transform" and "removed".
|
|
300
|
+
|
|
301
|
+
Element in transform are just array indices for where an element "was" in the previous
|
|
302
|
+
state. If the index is `-1`, that's the special case for when an element didn't exist before.
|
|
303
|
+
|
|
304
|
+
Element in the "removed" array are just elements that _were_ in the prev state, but not in the new
|
|
305
|
+
one.
|
|
306
|
+
|
|
307
|
+
We have some criteria to tell if something is considered "the same" (evaluated in this order):
|
|
308
|
+
1. Are they the same "type" of element? If not, they aren't the same.
|
|
309
|
+
2. Does either element have a key? If once of them does have a key,
|
|
310
|
+
and they don't match, then they aren't the same.
|
|
311
|
+
3. Do they appear in the same index? Then they are the same!
|
|
312
|
+
4. Otherwise, they are not the same.
|
|
313
|
+
|
|
314
|
+
We call this the `EqualityTest`, which we can represent as a function.
|
|
315
|
+
|
|
316
|
+
To combo all this up, we just run through both lists, marking stuff as visited
|
|
317
|
+
and add it to either "transform" or "removed" lists depending on
|
|
318
|
+
how it handles the equality test. Then, we take a look
|
|
319
|
+
through those lists, and create WorkTasks for each new change.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const tasks: WorkTask[] = [];
|
|
323
|
+
const newChildren: CommitRef[] = []
|
|
324
|
+
|
|
325
|
+
calculateDiff(result) {
|
|
326
|
+
// prevChildren was given to us by the Tree
|
|
327
|
+
const changeReport = generateChangeReport(this.prevChildren, result)
|
|
328
|
+
|
|
329
|
+
for (/* loop through "transform" */) {
|
|
330
|
+
newChildren.push(child.ref);
|
|
331
|
+
|
|
332
|
+
if (moved) {
|
|
333
|
+
tasks.push(WorkTask.moved(child))
|
|
334
|
+
} else if (created) {
|
|
335
|
+
tasks.push(WorkTask.fresh(child))
|
|
336
|
+
} else {
|
|
337
|
+
tasks.push(WorkTask.update(child))
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (/* loop through "removed" */) {
|
|
341
|
+
tasks.push(WorkTask.remove(child))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
generateChangeReport(prevs, nexts) {
|
|
346
|
+
const visited = new Set();
|
|
347
|
+
const transform = [];
|
|
348
|
+
const removed = [];
|
|
349
|
+
|
|
350
|
+
for (let nextIndex = 0; nextIndex < nexts.length; nextIndex++) {
|
|
351
|
+
const next = nexts[nextIndex];
|
|
352
|
+
const prevIndex = prevs.findIndex((prev, prevIndex) => equalityTest(prev, next, prevIndex, nextIndex));
|
|
353
|
+
transform.push(prevIndex);
|
|
354
|
+
if (prevIndex !== -1)
|
|
355
|
+
visited.add(prevIndex);
|
|
356
|
+
}
|
|
357
|
+
for (let i = 0; i < prevs.length; i++) {
|
|
358
|
+
if (!visited.has(i))
|
|
359
|
+
removed.push(i);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { transform, removed };
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## 8. Handling the Output
|
|
367
|
+
|
|
368
|
+
As you saw, the ElementOutput object bubbles all the way back up
|
|
369
|
+
to the Thread, and now we'll tackle the other half of the thread code
|
|
370
|
+
in the `work` function.
|
|
371
|
+
|
|
372
|
+
We update a few systems with the new output, such as:
|
|
373
|
+
- The Commit for the component is updated to reflect it's (possibly)
|
|
374
|
+
new children.
|
|
375
|
+
- The "previous" element, before the update, and the updated commit
|
|
376
|
+
are passed to a "Delta".
|
|
377
|
+
- The additional work tasks created are appended to the "tasks" array,
|
|
378
|
+
so our Thread will recursively keep following these instructions until
|
|
379
|
+
it "walks" through the entire affect tree.
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const tree: CommitTree2 = ...;
|
|
383
|
+
|
|
384
|
+
work() {
|
|
385
|
+
|
|
386
|
+
// ...as seen previously
|
|
387
|
+
const output = tree.processElement(task.commit.element, task.commit)
|
|
388
|
+
|
|
389
|
+
const oldElement = commit.element;
|
|
390
|
+
commit.update(element, output.newChildren);
|
|
391
|
+
|
|
392
|
+
this.delta.update(oldElement, commit);
|
|
393
|
+
|
|
394
|
+
this.tasks.push(...output.tasks);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## 9. Submitting the Delta
|
|
399
|
+
|
|
400
|
+
The "Delta" object that our thread updates represents all
|
|
401
|
+
the changes that a thread has recorded - containing
|
|
402
|
+
the "previous" and "next" state of any commit in a map. This structure
|
|
403
|
+
will be passed to the renderer when the thread runs out of tasks to do.
|
|
404
|
+
|
|
405
|
+
If a thread makes multiple changes to a single commit, only the final
|
|
406
|
+
change is recorded and sent to the renderer.
|
|
407
|
+
|
|
408
|
+
The thread discarded and a new one is created after it's delta is sent
|
|
409
|
+
to the renderer.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// inside delta
|
|
413
|
+
updates: Map<CommitID, { prev: Element, next: Commit2 }>
|
|
414
|
+
|
|
415
|
+
update(prev: Element, next: Commit2, moved: boolean) {
|
|
416
|
+
const change = this.changed.get(next.ref.id);
|
|
417
|
+
if (change) {
|
|
418
|
+
change.next = next;
|
|
419
|
+
} else {
|
|
420
|
+
this.changed.set(next.ref.id, { prev, next, moved });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// inside reconciler
|
|
425
|
+
onTasksComplete() {
|
|
426
|
+
// store the old thread
|
|
427
|
+
const currentThread = this.thread;
|
|
428
|
+
|
|
429
|
+
// Start a new thread
|
|
430
|
+
this.thread = new WorkThread2(this.tree);
|
|
431
|
+
|
|
432
|
+
// send delta
|
|
433
|
+
this.renderer.render(currentThread.delta);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## 10. Conclusion
|
|
438
|
+
|
|
439
|
+
And with that, our renderer receives a lovely
|
|
440
|
+
package of a "Delta", which it will
|
|
441
|
+
then use to modify the existing DOM
|
|
442
|
+
to update our button's text.
|
|
443
|
+
|
|
444
|
+
You can see that our Component visit task
|
|
445
|
+
probably created a few more Update tasks
|
|
446
|
+
of it's own - so our reconciler will be
|
|
447
|
+
working for a few cycles until it runs out of
|
|
448
|
+
tasks and submits our delta.
|
|
449
|
+
|
|
450
|
+
We didn't cover all topics, such as:
|
|
451
|
+
- **How do you start the process?** The "mount" process is really
|
|
452
|
+
just a special case of the "update" process, where there is just
|
|
453
|
+
no previous history, and everything is a `WorkTask.create`.
|
|
454
|
+
"unmount" is also similar, where there is history but no future state.
|
|
455
|
+
- **What happens to "missed" updates?** We mentions that
|
|
456
|
+
a thread never repeats a node if it's already rendered it, saving it
|
|
457
|
+
for another pass and marking it missed.
|
|
458
|
+
Essentially, when our renderer finishes all it's current tasks,
|
|
459
|
+
it checks it's "missed" array, and if there are entries there, it
|
|
460
|
+
just converts them to tasks and starts again from the top, clearing
|
|
461
|
+
its internal state (except for it's "Delta", which accumulates over
|
|
462
|
+
these passes). Only when both "missed" and "tasks" is empty is a thread
|
|
463
|
+
considered "done".
|
|
464
|
+
- **What happens after the Delta is sent to the renderer?** This
|
|
465
|
+
is better covered in the [Lifecycle of a Render](/)
|