Share
Embed
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
Transcript
89 slides
-
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 = 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 = 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 = 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 -
todos = signal<Todo[]>([]); count = computed(() => todos().length); effect(() =>
{ console.log('Todos count changed to ', count()); }); 1 2 3 4 5 6 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 -
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 -
todos = signal<Todo[]>([]); console.log('Todos: ' + todos()); effect(() => {
console.log('Todos', todos()); }); 1 2 3 4 5 6 7 -
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 -
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 -
console.log('Todos: ' + todos()); todos = signal<Todo[]>([]); 1 2 3
4 effect(() => { 5 console.log('Todos', todos()); 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 -
// 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 -
/** * 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 -
/** * 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 -
/** * 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 -
count = computed(() => todos().length); effect(() => { console.log('Todos count
changed to ', count()); }); todos = signal<Todo[]>([]); 1 2 3 4 5 6 7 -
count = computed(() => todos().length); effect(() => { console.log('just a
message'); }); todos = signal<Todo[]>([]); 1 2 3 4 5 6 7 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
Signal todos() Reactive Context effect() / HTML computed() version++ pull
value version++ pull value -
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 = 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 -
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 -
Signal todos() Reactive Context effect() / HTML computed() Same length!
version++ pull value -
computed(() => isEven() ? getDoneLength() : getUndoneLength() ); 1 2
3 4 [isEven, getDoneLength] [isEven, getUndoneLength] 1 2 3 [isEven, getDoneLength] 1 2 [isEven, getUndoneLength] 3 -
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