Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Angular Signals under the Hood

Angular Signals under the Hood

Dive deep into the inner workings of Angular signals with 'Angular Signals under the Hood'. This talk offers an in-depth exploration of Angular signals, uncovering the mechanics and nuances that power this advanced feature. We'll dissect the core concepts, architecture, and how signals enhance communication and performance in Angular applications. Ideal for developers seeking a thorough understanding of Angular signals, this presentation will provide you with the knowledge to leverage these tools effectively in your projects, ensuring optimized and future-proof Angular applications.

Fabian Gosebrink

May 24, 2024
Tweet

More Decks by Fabian Gosebrink

Other Decks in Technology

Transcript

  1. todos = signal<Todo[]>([]); todos(); todos.set(...); todos.update((items) => { /* ...

    */ }); 1 2 3 4 5 6 7 todos = signal<Todo[]>([]); 1 2 todos(); 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7
  2. todos = signal<Todo[]>([]); todos(); todos.set(...); todos.update((items) => { /* ...

    */ }); 1 2 3 4 5 6 7 todos = signal<Todo[]>([]); 1 2 todos(); 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7 todos(); todos = signal<Todo[]>([]); 1 2 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7
  3. todos = signal<Todo[]>([]); todos(); todos.set(...); todos.update((items) => { /* ...

    */ }); 1 2 3 4 5 6 7 todos = signal<Todo[]>([]); 1 2 todos(); 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7 todos(); todos = signal<Todo[]>([]); 1 2 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7 todos.set(...); todos = signal<Todo[]>([]); 1 2 todos(); 3 4 5 6 todos.update((items) => { /* ... */ }); 7
  4. todos = signal<Todo[]>([]); todos(); todos.set(...); todos.update((items) => { /* ...

    */ }); 1 2 3 4 5 6 7 todos = signal<Todo[]>([]); 1 2 todos(); 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7 todos(); todos = signal<Todo[]>([]); 1 2 3 4 todos.set(...); 5 6 todos.update((items) => { /* ... */ }); 7 todos.set(...); todos = signal<Todo[]>([]); 1 2 todos(); 3 4 5 6 todos.update((items) => { /* ... */ }); 7 todos.update((items) => { /* ... */ }); todos = signal<Todo[]>([]); 1 2 todos(); 3 4 todos.set(...); 5 6 7
  5. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); 1 2 3 4 5 6 7
  6. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); 1 2 3 4 5 6 7 count = computed(() => todos().length); todos = signal<Todo[]>([]); 1 2 3 4 effect(() => { 5 console.log('Todos count changed to ', count()); 6 }); 7
  7. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); 1 2 3 4 5 6 7 count = computed(() => todos().length); todos = signal<Todo[]>([]); 1 2 3 4 effect(() => { 5 console.log('Todos count changed to ', count()); 6 }); 7 effect(() => { console.log('Todos count changed to ', count()); }); todos = signal<Todo[]>([]); 1 2 count = computed(() => todos().length); 3 4 5 6 7
  8. todos = signal<Todo[]>([]); console.log('Todos: ' + todos()); effect(() => {

    console.log('Todos', todos()); }); 1 2 3 4 5 6 7
  9. todos = signal<Todo[]>([]); console.log('Todos: ' + todos()); effect(() => {

    console.log('Todos', todos()); }); 1 2 3 4 5 6 7 console.log('Todos: ' + todos()); todos = signal<Todo[]>([]); 1 2 3 4 effect(() => { 5 console.log('Todos', todos()); 6 }); 7
  10. todos = signal<Todo[]>([]); console.log('Todos: ' + todos()); effect(() => {

    console.log('Todos', todos()); }); 1 2 3 4 5 6 7 console.log('Todos: ' + todos()); todos = signal<Todo[]>([]); 1 2 3 4 effect(() => { 5 console.log('Todos', todos()); 6 }); 7 effect(() => { console.log('Todos', todos()); }); todos = signal<Todo[]>([]); 1 2 console.log('Todos: ' + todos()); 3 4 5 6 7
  11. console.log('Todos: ' + todos()); todos = signal<Todo[]>([]); 1 2 3

    4 effect(() => { 5 console.log('Todos', todos()); 6 }); 7
  12. console.log('Todos: ' + todos()); todos = signal<Todo[]>([]); 1 2 3

    4 effect(() => { 5 console.log('Todos', todos()); 6 }); 7 effect(() => { console.log('Todos', todos()); }); todos = signal<Todo[]>([]); 1 2 console.log('Todos: ' + todos()); 3 4 5 6 7
  13. // Reactive in TS effect(() => { console.log('Todos', todos()); });

    // Reactive in HTML <div>{{ todos() }}</div> todos = signal<Todo[]>([]); 1 2 console.log('Todos: ' + todos()); 3 4 5 6 7 8 9 10 11
  14. /** * Propagate a dirty notification to live consumers of

    this producer. */ export function producerNotifyConsumers(node: ReactiveNode): void { if (node.liveConsumerNode === undefined) { return; } // Prevent signal reads when we're updating the graph const prev = inNotificationPhase; inNotificationPhase = true; try { for (const consumer of node.liveConsumerNode) { if (!consumer.dirty) { consumerMarkDirty(consumer); } } } finally { inNotificationPhase = prev; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  15. /** * Propagate a dirty notification to live consumers of

    this producer. */ export function producerNotifyConsumers(node: ReactiveNode): void { if (node.liveConsumerNode === undefined) { return; } // Prevent signal reads when we're updating the graph const prev = inNotificationPhase; inNotificationPhase = true; try { for (const consumer of node.liveConsumerNode) { if (!consumer.dirty) { consumerMarkDirty(consumer); } } } finally { inNotificationPhase = prev; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function producerNotifyConsumers(node: ReactiveNode): void { /** 1 * Propagate a dirty notification to live consumers of this producer. 2 */ 3 4 if (node.liveConsumerNode === undefined) { 5 return; 6 } 7 8 // Prevent signal reads when we're updating the graph 9 const prev = inNotificationPhase; 10 inNotificationPhase = true; 11 try { 12 for (const consumer of node.liveConsumerNode) { 13 if (!consumer.dirty) { 14 consumerMarkDirty(consumer); 15 } 16 } 17 } finally { 18 inNotificationPhase = prev; 19 } 20 } 21
  16. /** * Propagate a dirty notification to live consumers of

    this producer. */ export function producerNotifyConsumers(node: ReactiveNode): void { if (node.liveConsumerNode === undefined) { return; } // Prevent signal reads when we're updating the graph const prev = inNotificationPhase; inNotificationPhase = true; try { for (const consumer of node.liveConsumerNode) { if (!consumer.dirty) { consumerMarkDirty(consumer); } } } finally { inNotificationPhase = prev; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function producerNotifyConsumers(node: ReactiveNode): void { /** 1 * Propagate a dirty notification to live consumers of this producer. 2 */ 3 4 if (node.liveConsumerNode === undefined) { 5 return; 6 } 7 8 // Prevent signal reads when we're updating the graph 9 const prev = inNotificationPhase; 10 inNotificationPhase = true; 11 try { 12 for (const consumer of node.liveConsumerNode) { 13 if (!consumer.dirty) { 14 consumerMarkDirty(consumer); 15 } 16 } 17 } finally { 18 inNotificationPhase = prev; 19 } 20 } 21 consumerMarkDirty(consumer); /** 1 * Propagate a dirty notification to live consumers of this producer. 2 */ 3 export function producerNotifyConsumers(node: ReactiveNode): void { 4 if (node.liveConsumerNode === undefined) { 5 return; 6 } 7 8 // Prevent signal reads when we're updating the graph 9 const prev = inNotificationPhase; 10 inNotificationPhase = true; 11 try { 12 for (const consumer of node.liveConsumerNode) { 13 if (!consumer.dirty) { 14 15 } 16 } 17 } finally { 18 inNotificationPhase = prev; 19 } 20 } 21
  17. count = computed(() => todos().length); effect(() => { console.log('Todos count

    changed to ', count()); }); todos = signal<Todo[]>([]); 1 2 3 4 5 6 7
  18. count = computed(() => todos().length); effect(() => { console.log('just a

    message'); }); todos = signal<Todo[]>([]); 1 2 3 4 5 6 7
  19. interface ConsumerNode extends ReactiveNode interface ProducerNode extends ReactiveNode export interface

    ReactiveNode { version: Version; lastCleanEpoch: Version; dirty: boolean; producerNode: ReactiveNode[] | undefined; producerLastReadVersion: Version[] | undefined; producerIndexOfThis: number[] | undefined; nextProducerIndex: number; liveConsumerNode: ReactiveNode[] | undefined; liveConsumerIndexOfThis: number[] | undefined; consumerAllowSignalWrites: boolean; readonly consumerIsAlwaysLive: boolean; producerMustRecompute(node: unknown): boolean; producerRecomputeValue(node: unknown): void; consumerMarkedDirty(node: unknown): void; consumerOnSignalRead(node: unknown): void; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
  20. interface ConsumerNode extends ReactiveNode interface ProducerNode extends ReactiveNode export interface

    ReactiveNode { version: Version; lastCleanEpoch: Version; dirty: boolean; producerNode: ReactiveNode[] | undefined; producerLastReadVersion: Version[] | undefined; producerIndexOfThis: number[] | undefined; nextProducerIndex: number; liveConsumerNode: ReactiveNode[] | undefined; liveConsumerIndexOfThis: number[] | undefined; consumerAllowSignalWrites: boolean; readonly consumerIsAlwaysLive: boolean; producerMustRecompute(node: unknown): boolean; producerRecomputeValue(node: unknown): void; consumerMarkedDirty(node: unknown): void; consumerOnSignalRead(node: unknown): void; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 dirty: boolean; interface ConsumerNode extends ReactiveNode 1 2 interface ProducerNode extends ReactiveNode 3 4 export interface ReactiveNode { 5 version: Version; 6 lastCleanEpoch: Version; 7 8 producerNode: ReactiveNode[] | undefined; 9 producerLastReadVersion: Version[] | undefined; 10 producerIndexOfThis: number[] | undefined; 11 nextProducerIndex: number; 12 liveConsumerNode: ReactiveNode[] | undefined; 13 liveConsumerIndexOfThis: number[] | undefined; 14 consumerAllowSignalWrites: boolean; 15 readonly consumerIsAlwaysLive: boolean; 16 producerMustRecompute(node: unknown): boolean; 17 producerRecomputeValue(node: unknown): void; 18 consumerMarkedDirty(node: unknown): void; 19 consumerOnSignalRead(node: unknown): void; 20 } 21
  21. interface ConsumerNode extends ReactiveNode interface ProducerNode extends ReactiveNode export interface

    ReactiveNode { version: Version; lastCleanEpoch: Version; dirty: boolean; producerNode: ReactiveNode[] | undefined; producerLastReadVersion: Version[] | undefined; producerIndexOfThis: number[] | undefined; nextProducerIndex: number; liveConsumerNode: ReactiveNode[] | undefined; liveConsumerIndexOfThis: number[] | undefined; consumerAllowSignalWrites: boolean; readonly consumerIsAlwaysLive: boolean; producerMustRecompute(node: unknown): boolean; producerRecomputeValue(node: unknown): void; consumerMarkedDirty(node: unknown): void; consumerOnSignalRead(node: unknown): void; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 dirty: boolean; interface ConsumerNode extends ReactiveNode 1 2 interface ProducerNode extends ReactiveNode 3 4 export interface ReactiveNode { 5 version: Version; 6 lastCleanEpoch: Version; 7 8 producerNode: ReactiveNode[] | undefined; 9 producerLastReadVersion: Version[] | undefined; 10 producerIndexOfThis: number[] | undefined; 11 nextProducerIndex: number; 12 liveConsumerNode: ReactiveNode[] | undefined; 13 liveConsumerIndexOfThis: number[] | undefined; 14 consumerAllowSignalWrites: boolean; 15 readonly consumerIsAlwaysLive: boolean; 16 producerMustRecompute(node: unknown): boolean; 17 producerRecomputeValue(node: unknown): void; 18 consumerMarkedDirty(node: unknown): void; 19 consumerOnSignalRead(node: unknown): void; 20 } 21 version: Version; dirty: boolean; interface ConsumerNode extends ReactiveNode 1 2 interface ProducerNode extends ReactiveNode 3 4 export interface ReactiveNode { 5 6 lastCleanEpoch: Version; 7 8 producerNode: ReactiveNode[] | undefined; 9 producerLastReadVersion: Version[] | undefined; 10 producerIndexOfThis: number[] | undefined; 11 nextProducerIndex: number; 12 liveConsumerNode: ReactiveNode[] | undefined; 13 liveConsumerIndexOfThis: number[] | undefined; 14 consumerAllowSignalWrites: boolean; 15 readonly consumerIsAlwaysLive: boolean; 16 producerMustRecompute(node: unknown): boolean; 17 producerRecomputeValue(node: unknown): void; 18 consumerMarkedDirty(node: unknown): void; 19 consumerOnSignalRead(node: unknown): void; 20 } 21
  22. export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { if (!producerUpdatesAllowed()) {

    throwInvalidWriteToSignalError(); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } 1 2 3 4 5 6 7 8 9 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 https://github.com/angular/angular/blob/1872fcd8e09fefb52f9b36e8261702cd6fb03f85/packages/core/primitives/signals/src/signal.ts
  23. export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { if (!producerUpdatesAllowed()) {

    throwInvalidWriteToSignalError(); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } 1 2 3 4 5 6 7 8 9 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 signalValueChanged(node); 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 https://github.com/angular/angular/blob/1872fcd8e09fefb52f9b36e8261702cd6fb03f85/packages/core/primitives/signals/src/signal.ts
  24. export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { if (!producerUpdatesAllowed()) {

    throwInvalidWriteToSignalError(); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } 1 2 3 4 5 6 7 8 9 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 signalValueChanged(node); 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 6 7 8 9 } 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 https://github.com/angular/angular/blob/1872fcd8e09fefb52f9b36e8261702cd6fb03f85/packages/core/primitives/signals/src/signal.ts
  25. export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { if (!producerUpdatesAllowed()) {

    throwInvalidWriteToSignalError(); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } 1 2 3 4 5 6 7 8 9 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 signalValueChanged(node); 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 6 7 8 9 } 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 signalValueChanged(node); function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 https://github.com/angular/angular/blob/1872fcd8e09fefb52f9b36e8261702cd6fb03f85/packages/core/primitives/signals/src/signal.ts
  26. export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { if (!producerUpdatesAllowed()) {

    throwInvalidWriteToSignalError(); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } 1 2 3 4 5 6 7 8 9 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 signalValueChanged(node); 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 6 7 8 9 } 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 node.version++; 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 signalValueChanged(node); function signalValueChanged<T>(node: SignalNode<T>): void { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(); } export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 8 } 9 } 10 11 // ... 12 13 14 15 16 17 18 19 signalValueChanged(node); node.version++; export function signalSetFn<T>(node: SignalNode<T>, newValue: T) { 1 if (!producerUpdatesAllowed()) { 2 throwInvalidWriteToSignalError(); 3 } 4 5 if (!node.equal(node.value, newValue)) { 6 node.value = newValue; 7 8 } 9 } 10 11 // ... 12 13 function signalValueChanged<T>(node: SignalNode<T>): void { 14 15 producerIncrementEpoch(); 16 producerNotifyConsumers(node); 17 postSignalSetFn?.(); 18 } 19 https://github.com/angular/angular/blob/1872fcd8e09fefb52f9b36e8261702cd6fb03f85/packages/core/primitives/signals/src/signal.ts
  27. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); todos.update((items) => [...items, { ... }]); // CHANGE 1 2 3 4 5 6 7 8 9
  28. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); todos.update((items) => [...items]); // "NO" CHANGE 1 2 3 4 5 6 7 8 9
  29. todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>

    { console.log('Todos count changed to ', count()); }); todos.update((items) => [...items]); // "NO" CHANGE 1 2 3 4 5 6 7 8 9 todos.update((items) => [...items]); // "NO" CHANGE todos = signal<Todo[]>([]); 1 2 count = computed(() => todos().length); 3 4 effect(() => { 5 console.log('Todos count changed to ', count()); 6 }); 7 8 9
  30. todos.update((items) => [...items]); // Only ref change todos = signal<Todo[]>([]);

    1 2 count = computed(() => todos().length); 3 4 effect(() => { 5 console.log('Todos count changed to ', count()); 6 }); 7 8 9 export function defaultEquals<T>(a: T, b: T) { return Object.is(a, b); } 1 2 3 https://github.com/angular/angular/blob/23eafb4aa20f45233b22a62fe032e47ac21eca20/packages/core/primitives/signals/src/equality.ts#L17
  31. computed(() => isEven() ? getDoneLength() : getUndoneLength() ); 1 2

    3 4 [isEven, getDoneLength] [isEven, getUndoneLength] 1 2 3 [isEven, getDoneLength] 1 2 [isEven, getUndoneLength] 3
  32. computed(() => isEven() ? getDoneLength() : getUndoneLength() ); 1 2

    3 4 [isEven, getDoneLength] [isEven, getUndoneLength] 1 2 3 [isEven, getDoneLength] 1 2 [isEven, getUndoneLength] 3 [isEven, getUndoneLength] [isEven, getDoneLength] 1 2 3