Skip to content

Commit 9d3ffd9

Browse files
authored
refactor: preserve dependency order (#265)
1 parent f39d726 commit 9d3ffd9

File tree

1 file changed

+101
-35
lines changed

1 file changed

+101
-35
lines changed

packages/core/src/index.ts

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,32 @@ function addDependency(signal: Signal): Node | undefined {
110110

111111
let node = signal._node;
112112
if (node === undefined || node._target !== evalContext) {
113-
// `signal` is a new dependency. Create a new node dependency node, move it
114-
// to the front of the current context's dependency list.
113+
/**
114+
* `signal` is a new dependency. Create a new dependency node, and set it
115+
* as the tail of the current context's dependency list. e.g:
116+
*
117+
* { A <-> B }
118+
* ↑ ↑
119+
* tail node (new)
120+
* ↓
121+
* { A <-> B <-> C }
122+
* ↑
123+
* tail (evalContext._sources)
124+
*/
115125
node = {
116126
_version: 0,
117127
_source: signal,
118-
_prevSource: undefined,
119-
_nextSource: evalContext._sources,
128+
_prevSource: evalContext._sources,
129+
_nextSource: undefined,
120130
_target: evalContext,
121131
_prevTarget: undefined,
122132
_nextTarget: undefined,
123133
_rollbackNode: node,
124134
};
135+
136+
if (evalContext._sources !== undefined) {
137+
evalContext._sources._nextSource = node;
138+
}
125139
evalContext._sources = node;
126140
signal._node = node;
127141

@@ -135,18 +149,30 @@ function addDependency(signal: Signal): Node | undefined {
135149
// `signal` is an existing dependency from a previous evaluation. Reuse it.
136150
node._version = 0;
137151

138-
// If `node` is not already the current head of the dependency list (i.e.
139-
// there is a previous node in the list), then make `node` the new head.
140-
if (node._prevSource !== undefined) {
141-
node._prevSource._nextSource = node._nextSource;
142-
if (node._nextSource !== undefined) {
143-
node._nextSource._prevSource = node._prevSource;
152+
/**
153+
* If `node` is not already the current tail of the dependency list (i.e.
154+
* there is a next node in the list), then make the `node` the new tail. e.g:
155+
*
156+
* { A <-> B <-> C <-> D }
157+
* ↑ ↑
158+
* node ┌─── tail (evalContext._sources)
159+
* └─────│─────┐
160+
* ↓ ↓
161+
* { A <-> C <-> D <-> B }
162+
* ↑
163+
* tail (evalContext._sources)
164+
*/
165+
if (node._nextSource !== undefined) {
166+
node._nextSource._prevSource = node._prevSource;
167+
168+
if (node._prevSource !== undefined) {
169+
node._prevSource._nextSource = node._nextSource;
144170
}
145-
node._prevSource = undefined;
146-
node._nextSource = evalContext._sources;
147-
// evalCotext._sources must be !== undefined (and !== node), because
148-
// `node` was originally pointing to some previous node.
149-
evalContext._sources!._prevSource = node;
171+
172+
node._prevSource = evalContext._sources;
173+
node._nextSource = undefined;
174+
175+
evalContext._sources!._nextSource = node;
150176
evalContext._sources = node;
151177
}
152178

@@ -161,7 +187,8 @@ declare class Signal<T = any> {
161187
/** @internal */
162188
_value: unknown;
163189

164-
/** @internal
190+
/**
191+
* @internal
165192
* Version numbers should always be >= 0, because the special value -1 is used
166193
* by Nodes to signify potentially unused but recyclable nodes.
167194
*/
@@ -321,12 +348,24 @@ function needsToRecompute(target: Computed | Effect): boolean {
321348
return true;
322349
}
323350
}
324-
// If none of the dependencies have changed values since last recompute then the
351+
// If none of the dependencies have changed values since last recompute then
325352
// there's no need to recompute.
326353
return false;
327354
}
328355

329356
function prepareSources(target: Computed | Effect) {
357+
/**
358+
* 1. Mark all current sources as re-usable nodes (version: -1)
359+
* 2. Set a rollback node if the current node is being used in a different context
360+
* 3. Point 'target._sources' to the tail of the doubly-linked list, e.g:
361+
*
362+
* { undefined <- A <-> B <-> C -> undefined }
363+
* ↑ ↑
364+
* │ └──────┐
365+
* target._sources = A; (node is head) │
366+
* ↓ │
367+
* target._sources = C; (node is tail) ─┘
368+
*/
330369
for (
331370
let node = target._sources;
332371
node !== undefined;
@@ -338,39 +377,66 @@ function prepareSources(target: Computed | Effect) {
338377
}
339378
node._source._node = node;
340379
node._version = -1;
380+
381+
if (node._nextSource === undefined) {
382+
target._sources = node;
383+
break;
384+
}
341385
}
342386
}
343387

344388
function cleanupSources(target: Computed | Effect) {
345-
// At this point target._sources is a mishmash of current & former dependencies.
346-
// The current dependencies are also in a reverse order of use.
347-
// Therefore build a new, reverted list of dependencies containing only the current
348-
// dependencies in a proper order of use.
349-
// Drop former dependencies from the list and unsubscribe from their change notifications.
350-
351389
let node = target._sources;
352-
let sources = undefined;
390+
let head = undefined;
391+
392+
/**
393+
* At this point 'target._sources' points to the tail of the doubly-linked list.
394+
* It contains all existing sources + new sources in order of use.
395+
* Iterate backwards until we find the head node while dropping old dependencies.
396+
*/
353397
while (node !== undefined) {
354-
const next = node._nextSource;
398+
const prev = node._prevSource;
399+
400+
/**
401+
* The node was not re-used, unsubscribe from its change notifications and remove itself
402+
* from the doubly-linked list. e.g:
403+
*
404+
* { A <-> B <-> C }
405+
* ↓
406+
* { A <-> C }
407+
*/
355408
if (node._version === -1) {
356409
node._source._unsubscribe(node);
357-
node._nextSource = undefined;
358-
} else {
359-
if (sources !== undefined) {
360-
sources._prevSource = node;
410+
411+
if (prev !== undefined) {
412+
prev._nextSource = node._nextSource;
361413
}
362-
node._prevSource = undefined;
363-
node._nextSource = sources;
364-
sources = node;
414+
if (node._nextSource !== undefined) {
415+
node._nextSource._prevSource = prev;
416+
}
417+
} else {
418+
/**
419+
* The new head is the last node seen which wasn't removed/unsubscribed
420+
* from the doubly-linked list. e.g:
421+
*
422+
* { A <-> B <-> C }
423+
* ↑ ↑ ↑
424+
* │ │ └ head = node
425+
* │ └ head = node
426+
* └ head = node
427+
*/
428+
head = node;
365429
}
366430

367431
node._source._node = node._rollbackNode;
368432
if (node._rollbackNode !== undefined) {
369433
node._rollbackNode = undefined;
370434
}
371-
node = next;
435+
436+
node = prev;
372437
}
373-
target._sources = sources;
438+
439+
target._sources = head;
374440
}
375441

376442
declare class Computed<T = any> extends Signal<T> {
@@ -417,7 +483,7 @@ Computed.prototype._refresh = function () {
417483
this._globalVersion = globalVersion;
418484

419485
// Mark this computed signal running before checking the dependencies for value
420-
// changes, so that the RUNNIN flag can be used to notice cyclical dependencies.
486+
// changes, so that the RUNNING flag can be used to notice cyclical dependencies.
421487
this._flags |= RUNNING;
422488
if (this._version > 0 && !needsToRecompute(this)) {
423489
this._flags &= ~RUNNING;

0 commit comments

Comments
 (0)