From fa451bcd19bcebfc3dca60265fea4f08beee1cde Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Fri, 9 Mar 2018 12:45:31 -0800 Subject: [PATCH] feat(ivy): support markForCheck (#22690) PR Close #22690 --- .../change_detection/change_detector_ref.ts | 26 +-- packages/core/src/render3/instructions.ts | 2 +- packages/core/src/render3/view_ref.ts | 162 +++++++++++++++++- .../test/render3/change_detection_spec.ts | 155 +++++++++++++++++ 4 files changed, 327 insertions(+), 18 deletions(-) diff --git a/packages/core/src/change_detection/change_detector_ref.ts b/packages/core/src/change_detection/change_detector_ref.ts index 92372f3480..dcf25fc3b5 100644 --- a/packages/core/src/change_detection/change_detector_ref.ts +++ b/packages/core/src/change_detection/change_detector_ref.ts @@ -11,7 +11,11 @@ */ export abstract class ChangeDetectorRef { /** - * Marks all {@link ChangeDetectionStrategy#OnPush OnPush} ancestors as to be checked. + * Marks a view and all of its ancestors dirty. + * + * This can be used to ensure an {@link ChangeDetectionStrategy#OnPush OnPush} component is + * checked when it needs to be re-rendered but the two normal triggers haven't marked it + * dirty (i.e. inputs haven't changed and events haven't fired in the view). * * * @@ -39,12 +43,12 @@ export abstract class ChangeDetectorRef { abstract markForCheck(): void; /** - * Detaches the change detector from the change detector tree. + * Detaches the view from the change detection tree. * - * The detached change detector will not be checked until it is reattached. - * - * This can also be used in combination with {@link ChangeDetectorRef#detectChanges detectChanges} - * to implement local change detection checks. + * Detached views will not be checked during change detection runs until they are + * re-attached, even if they are dirty. `detach` can be used in combination with + * {@link ChangeDetectorRef#detectChanges detectChanges} to implement local change + * detection checks. * * * @@ -93,7 +97,7 @@ export abstract class ChangeDetectorRef { abstract detach(): void; /** - * Checks the change detector and its children. + * Checks the view and its children. * * This can also be used in combination with {@link ChangeDetectorRef#detach detach} to implement * local change detection checks. @@ -108,8 +112,7 @@ export abstract class ChangeDetectorRef { * we want to check and update the list every five seconds. * * We can do that by detaching the component's change detector and doing a local change detection - * check - * every five seconds. + * check every five seconds. * * See {@link ChangeDetectorRef#detach detach} for more information. */ @@ -124,7 +127,10 @@ export abstract class ChangeDetectorRef { abstract checkNoChanges(): void; /** - * Reattach the change detector to the change detector tree. + * Re-attaches the view to the change detection tree. + * + * This can be used to re-attach views that were previously detached from the tree + * using {@link ChangeDetectorRef#detach detach}. Views are attached to the tree by default. * * * diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 10ef97b088..af89965760 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -1499,7 +1499,7 @@ export function wrapListenerWithDirtyAndDefault( } /** Marks current view and all ancestors dirty */ -function markViewDirty(view: LView): void { +export function markViewDirty(view: LView): void { let currentView: LView|null = view; while (currentView.parent != null) { diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 422299fefa..3b52c44554 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -8,7 +8,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref'; -import {detectChanges} from './instructions'; +import {detectChanges, markViewDirty} from './instructions'; import {ComponentTemplate} from './interfaces/definition'; import {LViewNode} from './interfaces/node'; import {LView, LViewFlags} from './interfaces/view'; @@ -26,14 +26,93 @@ export class ViewRef implements viewEngine_EmbeddedViewRef { destroy(): void { notImplemented(); } destroyed: boolean; onDestroy(callback: Function) { notImplemented(); } - markForCheck(): void { notImplemented(); } /** - * Detaches a view from the change detection tree. + * Marks a view and all of its ancestors dirty. * - * Detached views will not be checked during change detection runs, even if the view - * is dirty. This can be used in combination with detectChanges to implement local - * change detection checks. + * It also triggers change detection by calling `scheduleTick` internally, which coalesces + * multiple `markForCheck` calls to into one change detection run. + * + * This can be used to ensure an {@link ChangeDetectionStrategy#OnPush OnPush} component is + * checked when it needs to be re-rendered but the two normal triggers haven't marked it + * dirty (i.e. inputs haven't changed and events haven't fired in the view). + * + * + * + * ### Example ([live demo](https://stackblitz.com/edit/angular-kx7rrw)) + * + * ```typescript + * @Component({ + * selector: 'my-app', + * template: `Number of ticks: {{numberOfTicks}}` + * changeDetection: ChangeDetectionStrategy.OnPush, + * }) + * class AppComponent { + * numberOfTicks = 0; + * + * constructor(private ref: ChangeDetectorRef) { + * setInterval(() => { + * this.numberOfTicks++; + * // the following is required, otherwise the view will not be updated + * this.ref.markForCheck(); + * }, 1000); + * } + * } + * ``` + */ + markForCheck(): void { markViewDirty(this._view); } + + /** + * Detaches the view from the change detection tree. + * + * Detached views will not be checked during change detection runs until they are + * re-attached, even if they are dirty. `detach` can be used in combination with + * {@link ChangeDetectorRef#detectChanges detectChanges} to implement local change + * detection checks. + * + * + * + * + * ### Example + * + * The following example defines a component with a large list of readonly data. + * Imagine the data changes constantly, many times per second. For performance reasons, + * we want to check and update the list every five seconds. We can do that by detaching + * the component's change detector and doing a local check every five seconds. + * + * ```typescript + * class DataProvider { + * // in a real application the returned data will be different every time + * get data() { + * return [1,2,3,4,5]; + * } + * } + * + * @Component({ + * selector: 'giant-list', + * template: ` + *
  • Data {{d}}
  • + * `, + * }) + * class GiantList { + * constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) { + * ref.detach(); + * setInterval(() => { + * this.ref.detectChanges(); + * }, 5000); + * } + * } + * + * @Component({ + * selector: 'app', + * providers: [DataProvider], + * template: ` + * + * `, + * }) + * class App { + * } + * ``` */ detach(): void { this._view.flags &= ~LViewFlags.Attached; } @@ -41,10 +120,79 @@ export class ViewRef implements viewEngine_EmbeddedViewRef { * Re-attaches a view to the change detection tree. * * This can be used to re-attach views that were previously detached from the tree - * using detach(). Views are attached to the tree by default. + * using {@link ChangeDetectorRef#detach detach}. Views are attached to the tree by default. + * + * + * + * ### Example ([live demo](https://stackblitz.com/edit/angular-ymgsxw)) + * + * The following example creates a component displaying `live` data. The component will detach + * its change detector from the main change detector tree when the component's live property + * is set to false. + * + * ```typescript + * class DataProvider { + * data = 1; + * + * constructor() { + * setInterval(() => { + * this.data = this.data * 2; + * }, 500); + * } + * } + * + * @Component({ + * selector: 'live-data', + * inputs: ['live'], + * template: 'Data: {{dataProvider.data}}' + * }) + * class LiveData { + * constructor(private ref: ChangeDetectorRef, private dataProvider: DataProvider) {} + * + * set live(value) { + * if (value) { + * this.ref.reattach(); + * } else { + * this.ref.detach(); + * } + * } + * } + * + * @Component({ + * selector: 'my-app', + * providers: [DataProvider], + * template: ` + * Live Update: + * + * `, + * }) + * class AppComponent { + * live = true; + * } + * ``` */ reattach(): void { this._view.flags |= LViewFlags.Attached; } + /** + * Checks the view and its children. + * + * This can also be used in combination with {@link ChangeDetectorRef#detach detach} to implement + * local change detection checks. + * + * + * + * + * ### Example + * + * The following example defines a component with a large list of readonly data. + * Imagine, the data changes constantly, many times per second. For performance reasons, + * we want to check and update the list every five seconds. + * + * We can do that by detaching the component's change detector and doing a local change detection + * check every five seconds. + * + * See {@link ChangeDetectorRef#detach detach} for more information. + */ detectChanges(): void { detectChanges(this.context); } checkNoChanges(): void { notImplemented(); } diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index 4dd97fbaa1..281b604663 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -756,6 +756,161 @@ describe('change detection', () => { }); + describe('markForCheck()', () => { + let comp: OnPushComp; + + class OnPushComp { + value = 'one'; + + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + + static ngComponentDef = defineComponent({ + type: OnPushComp, + tag: 'on-push-comp', + factory: () => comp = new OnPushComp(injectChangeDetectorRef()), + /** {{ value }} */ + template: (ctx: OnPushComp, cm: boolean) => { + if (cm) { + text(0); + } + textBinding(0, bind(ctx.value)); + }, + changeDetection: ChangeDetectionStrategy.OnPush + }); + } + + class OnPushParent { + value = 'one'; + + static ngComponentDef = defineComponent({ + type: OnPushParent, + tag: 'on-push-parent', + factory: () => new OnPushParent(), + /** + * {{ value }} - + * + */ + template: (ctx: OnPushParent, cm: boolean) => { + if (cm) { + text(0); + elementStart(1, OnPushComp); + elementEnd(); + } + textBinding(0, interpolation1('', ctx.value, ' - ')); + OnPushComp.ngComponentDef.h(2, 1); + directiveRefresh(2, 1); + }, + changeDetection: ChangeDetectionStrategy.OnPush + }); + } + + it('should schedule check on OnPush components', () => { + const parent = renderComponent(OnPushParent); + expect(getRenderedText(parent)).toEqual('one - one'); + + comp.value = 'two'; + tick(parent); + expect(getRenderedText(parent)).toEqual('one - one'); + + comp.cdr.markForCheck(); + requestAnimationFrame.flush(); + expect(getRenderedText(parent)).toEqual('one - two'); + }); + + it('should only run change detection once with multiple calls to markForCheck', () => { + renderComponent(OnPushParent); + expect(comp.doCheckCount).toEqual(1); + + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + requestAnimationFrame.flush(); + + expect(comp.doCheckCount).toEqual(2); + }); + + it('should schedule check on ancestor OnPush components', () => { + const parent = renderComponent(OnPushParent); + expect(getRenderedText(parent)).toEqual('one - one'); + + parent.value = 'two'; + tick(parent); + expect(getRenderedText(parent)).toEqual('one - one'); + + comp.cdr.markForCheck(); + requestAnimationFrame.flush(); + expect(getRenderedText(parent)).toEqual('two - one'); + + }); + + it('should schedule check on OnPush components in embedded views', () => { + class EmbeddedViewParent { + value = 'one'; + showing = true; + + static ngComponentDef = defineComponent({ + type: EmbeddedViewParent, + tag: 'embedded-view-parent', + factory: () => new EmbeddedViewParent(), + /** + * {{ value }} - + * % if (ctx.showing) { + * + * % } + */ + template: (ctx: EmbeddedViewParent, cm: boolean) => { + if (cm) { + text(0); + container(1); + } + textBinding(0, interpolation1('', ctx.value, ' - ')); + containerRefreshStart(1); + { + if (ctx.showing) { + if (embeddedViewStart(0)) { + elementStart(0, OnPushComp); + elementEnd(); + } + OnPushComp.ngComponentDef.h(1, 0); + directiveRefresh(1, 0); + embeddedViewEnd(); + } + } + containerRefreshEnd(); + }, + changeDetection: ChangeDetectionStrategy.OnPush + }); + } + + const parent = renderComponent(EmbeddedViewParent); + expect(getRenderedText(parent)).toEqual('one - one'); + + comp.value = 'two'; + tick(parent); + expect(getRenderedText(parent)).toEqual('one - one'); + + comp.cdr.markForCheck(); + requestAnimationFrame.flush(); + expect(getRenderedText(parent)).toEqual('one - two'); + + parent.value = 'two'; + tick(parent); + expect(getRenderedText(parent)).toEqual('one - two'); + + comp.cdr.markForCheck(); + requestAnimationFrame.flush(); + expect(getRenderedText(parent)).toEqual('two - two'); + }); + + // TODO(kara): add test for dynamic views once bug fix is in + }); + }); });