refactor(ivy): Add newer, smaller NgOnChangesFeature (#28187)

PR Close #28187
This commit is contained in:
Ben Lesh
2019-01-16 09:35:35 -08:00
committed by Alex Rickabaugh
parent 5552661fd7
commit a95e81978b
22 changed files with 752 additions and 1071 deletions

View File

@ -64,20 +64,6 @@ export class WrappedValue {
static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; }
}
/**
* Represents a basic change from a previous to a new value.
*
* @publicApi
*/
export class SimpleChange {
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
/**
* Check whether the new value is the first value assigned.
*/
isFirstChange(): boolean { return this.firstChange; }
}
export function isListLikeIterable(obj: any): boolean {
if (!isJsObject(obj)) return false;
return Array.isArray(obj) ||

View File

@ -5,19 +5,9 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChanges, SimpleChange} from './simple_change';
import {SimpleChanges} from './simple_change';
/**
* Defines an object that associates properties with
* instances of `SimpleChange`.
*
* @see `OnChanges`
*
* @publicApi
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }
/**
* @description
* A lifecycle hook that is called when any data-bound property of a directive changes.

View File

@ -17,6 +17,7 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {publishDefaultGlobalUtils} from './global_utils';
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
@ -25,7 +26,6 @@ import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './inte
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
import { registerPreOrderHooks, registerPostOrderHooks } from './hooks';
@ -240,7 +240,8 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
registerPreOrderHooks(dirIndex, def, rootTView);
// TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
// LNode).
registerPostOrderHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
registerPostOrderHooks(
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
}
/**

View File

@ -276,6 +276,7 @@ export function defineComponent<T>(componentDefinition: {
id: 'c',
styles: componentDefinition.styles || EMPTY_ARRAY,
_: null as never,
setInput: null,
};
def._ = noSideEffects(() => {
const directiveTypes = componentDefinition.directives !;
@ -380,12 +381,14 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
*
*/
function invertObject(obj: any, secondary?: any): any {
if (obj == null) return EMPTY_OBJ;
function invertObject<T>(
obj?: {[P in keyof T]?: string | [string, string]},
secondary?: {[key: string]: string}): {[P in keyof T]: string} {
if (obj == null) return EMPTY_OBJ as any;
const newLookup: any = {};
for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) {
let publicName: string = obj[minifiedKey];
let publicName: string|[string, string] = obj[minifiedKey] !;
let declaredName = publicName;
if (Array.isArray(publicName)) {
declaredName = publicName[1];
@ -393,7 +396,7 @@ function invertObject(obj: any, secondary?: any): any {
}
newLookup[publicName] = minifiedKey;
if (secondary) {
(secondary[publicName] = declaredName);
(secondary[publicName] = declaredName as string);
}
}
}
@ -470,11 +473,11 @@ export function defineBase<T>(baseDefinition: {
*/
outputs?: {[P in keyof T]?: string};
}): BaseDef<T> {
const declaredInputs: {[P in keyof T]: P} = {} as any;
const declaredInputs: {[P in keyof T]: string} = {} as any;
return {
inputs: invertObject(baseDefinition.inputs, declaredInputs),
inputs: invertObject<T>(baseDefinition.inputs as any, declaredInputs),
declaredInputs: declaredInputs,
outputs: invertObject(baseDefinition.outputs),
outputs: invertObject<T>(baseDefinition.outputs as any),
};
}

View File

@ -7,10 +7,10 @@
*/
import {Type} from '../../interface/type';
import {Component} from '../../metadata/directives';
import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import { Component } from '../../metadata/directives';

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChange} from '../../change_detection/change_detection_util';
import {SimpleChanges} from '../../interface/simple_change';
import {OnChanges} from '../../interface/lifecycle_hooks';
import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
import {EMPTY_OBJ} from '../empty';
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
const PRIVATE_PREFIX = '__ngOnChanges_';
@ -40,86 +40,63 @@ type OnChangesExpando = OnChanges & {
* ```
*/
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
const publicToDeclaredInputs = definition.declaredInputs;
const publicToMinifiedInputs = definition.inputs;
const proto = definition.type.prototype;
for (const publicName in publicToDeclaredInputs) {
if (publicToDeclaredInputs.hasOwnProperty(publicName)) {
const minifiedKey = publicToMinifiedInputs[publicName];
const declaredKey = publicToDeclaredInputs[publicName];
const privateMinKey = PRIVATE_PREFIX + minifiedKey;
if (definition.type.prototype.ngOnChanges) {
definition.setInput = ngOnChangesSetInput;
// Walk the prototype chain to see if we find a property descriptor
// That way we can honor setters and getters that were inherited.
let originalProperty: PropertyDescriptor|undefined = undefined;
let checkProto = proto;
while (!originalProperty && checkProto &&
Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) {
originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey);
checkProto = Object.getPrototypeOf(checkProto);
}
const prevDoCheck = definition.doCheck;
const prevOnInit = definition.onInit;
const getter = originalProperty && originalProperty.get;
const setter = originalProperty && originalProperty.set;
definition.onInit = wrapOnChanges(prevOnInit);
definition.doCheck = wrapOnChanges(prevDoCheck);
}
}
// create a getter and setter for property
Object.defineProperty(proto, minifiedKey, {
get: getter ||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
set<T>(this: OnChangesExpando, value: T) {
let simpleChanges = this[PRIVATE_PREFIX];
if (!simpleChanges) {
simpleChanges = {};
// Place where we will store SimpleChanges if there is a change
Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true});
}
function wrapOnChanges(hook: (() => void) | null) {
return function(this: OnChanges) {
const simpleChangesStore = getSimpleChangesStore(this);
const current = simpleChangesStore && simpleChangesStore.current;
const isFirstChange = !this.hasOwnProperty(privateMinKey);
const currentChange = simpleChanges[declaredKey];
if (currentChange) {
currentChange.currentValue = value;
} else {
simpleChanges[declaredKey] =
new SimpleChange(this[privateMinKey], value, isFirstChange);
}
if (isFirstChange) {
// Create a place where the actual value will be stored and make it non-enumerable
Object.defineProperty(this, privateMinKey, {value, writable: true});
} else {
this[privateMinKey] = value;
}
if (setter) setter.call(this, value);
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode
});
if (current) {
simpleChangesStore !.previous = current;
simpleChangesStore !.current = null;
this.ngOnChanges(current);
}
}
// If an onInit hook is defined, it will need to wrap the ngOnChanges call
// so the call order is changes-init-check in creation mode. In subsequent
// change detection runs, only the check wrapper will be called.
if (definition.onInit != null) {
definition.onInit = onChangesWrapper(definition.onInit);
}
hook && hook.call(this);
};
}
definition.doCheck = onChangesWrapper(definition.doCheck);
function ngOnChangesSetInput<T>(
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
const simpleChangesStore = getSimpleChangesStore(instance) ||
setSimpleChangesStore(instance, {previous: EMPTY_OBJ, current: null});
const current = simpleChangesStore.current || (simpleChangesStore.current = {});
const previous = simpleChangesStore.previous;
const declaredName = (this.declaredInputs as{[key: string]: string})[publicName];
const previousChange = previous[declaredName];
current[declaredName] = new SimpleChange(
previousChange && previousChange.currentValue, value, previous === EMPTY_OBJ);
(instance as any)[privateName] = value;
}
const SIMPLE_CHANGES_STORE = '__ngSimpleChanges__';
function getSimpleChangesStore(instance: any): null|NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] || null;
}
function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] = store;
}
// This option ensures that the ngOnChanges lifecycle hook will be inherited
// from superclasses (in InheritDefinitionFeature).
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) {
const simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
if (delegateHook) delegateHook.apply(this);
};
interface NgSimpleChangesStore {
previous: SimpleChanges;
current: SimpleChanges|null;
}

View File

@ -959,10 +959,10 @@ function listenerInternal(
const propsLength = props.length;
if (propsLength) {
const lCleanup = getCleanup(lView);
for (let i = 0; i < propsLength; i += 2) {
for (let i = 0; i < propsLength; i += 3) {
const index = props[i] as number;
ngDevMode && assertDataInRange(lView, index);
const minifiedName = props[i + 1];
const minifiedName = props[i + 2];
const directiveInstance = lView[index];
const output = directiveInstance[minifiedName];
@ -1214,18 +1214,29 @@ export function createTNode(
* @param value Value to set.
*/
function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void {
for (let i = 0; i < inputs.length; i += 2) {
ngDevMode && assertDataInRange(lView, inputs[i] as number);
lView[inputs[i] as number][inputs[i + 1]] = value;
const tView = lView[TVIEW];
for (let i = 0; i < inputs.length;) {
const index = inputs[i++] as number;
const publicName = inputs[i++] as string;
const privateName = inputs[i++] as string;
const instance = lView[index];
ngDevMode && assertDataInRange(lView, index);
const def = tView.data[index] as DirectiveDef<any>;
const setInput = def.setInput;
if (setInput) {
def.setInput !(instance, value, publicName, privateName);
} else {
instance[privateName] = value;
}
}
}
function setNgReflectProperties(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
value: any) {
for (let i = 0; i < inputs.length; i += 2) {
for (let i = 0; i < inputs.length; i += 3) {
const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
const attrName = normalizeDebugBindingName(inputs[i + 2] as string);
const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ?
@ -1268,8 +1279,8 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
propStore = propStore || {};
const internalName = propertyAliasMap[publicName];
const hasProperty = propStore.hasOwnProperty(publicName);
hasProperty ? propStore[publicName].push(i, internalName) :
(propStore[publicName] = [i, internalName]);
hasProperty ? propStore[publicName].push(i, publicName, internalName) :
(propStore[publicName] = [i, publicName, internalName]);
}
}
}
@ -1702,7 +1713,7 @@ function postProcessDirective<T>(
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def);
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
if (previousOrParentTNode && previousOrParentTNode.attrs) {
setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode);
setInputsFromAttrs(directiveDefIdx, directive, def, previousOrParentTNode);
}
if (def.contentQueries) {
@ -1903,16 +1914,24 @@ function addComponentLogic<T>(
* @param tNode The static data for this node
*/
function setInputsFromAttrs<T>(
directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void {
directiveIndex: number, instance: T, def: DirectiveDef<T>, tNode: TNode): void {
let initialInputData = tNode.initialInputs as InitialInputData | undefined;
if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
initialInputData = generateInitialInputs(directiveIndex, inputs, tNode);
initialInputData = generateInitialInputs(directiveIndex, def.inputs, tNode);
}
const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
if (initialInputs) {
for (let i = 0; i < initialInputs.length; i += 2) {
(instance as any)[initialInputs[i]] = initialInputs[i + 1];
const setInput = def.setInput;
for (let i = 0; i < initialInputs.length;) {
const publicName = initialInputs[i++];
const privateName = initialInputs[i++];
const value = initialInputs[i++];
if (setInput) {
def.setInput !(instance, value, publicName, privateName);
} else {
(instance as any)[privateName] = value;
}
}
}
}
@ -1956,7 +1975,7 @@ function generateInitialInputs(
if (minifiedInputName !== undefined) {
const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrValue as string);
inputsToStore.push(attrName, minifiedInputName, attrValue as string);
}
i += 2;

View File

@ -84,14 +84,14 @@ export interface BaseDef<T> {
* @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of
* public or minified name.
*/
readonly declaredInputs: {[P in keyof T]: P};
readonly declaredInputs: {[P in keyof T]: string};
/**
* A dictionary mapping the outputs' minified property names to their public API names, which
* are their aliases if any, or their original unminified property names
* (as in `@Output('alias') propertyName: any;`).
*/
readonly outputs: {[P in keyof T]: P};
readonly outputs: {[P in keyof T]: string};
}
/**
@ -152,6 +152,10 @@ export interface DirectiveDef<T> extends BaseDef<T> {
* The features applied to this directive
*/
readonly features: DirectiveDefFeature[]|null;
setInput:
((this: DirectiveDef<T>, instance: T, value: any, publicName: string,
privateName: string) => void)|null;
}
export type ComponentDefWithMeta<

View File

@ -468,10 +468,11 @@ export type PropertyAliases = {
/**
* Store the runtime input or output names for all the directives.
*
* - Even indices: directive index
* - Odd indices: minified / internal name
* i+0: directive instance index
* i+1: publicName
* i+2: privateName
*
* e.g. [0, 'change-minified']
* e.g. [0, 'change', 'change-minified']
*/
export type PropertyAliasValue = (number | string)[];
@ -484,14 +485,15 @@ export type PropertyAliasValue = (number | string)[];
*
* Within each sub-array:
*
* Even indices: minified/internal input name
* Odd indices: initial value
* i+0: attribute name
* i+1: minified/internal input name
* i+2: initial value
*
* If a directive on a node does not have any input properties
* that should be set from attributes, its index is set to null
* to avoid a sparse array.
*
* e.g. [null, ['role-min', 'button']]
* e.g. [null, ['role-min', 'minified-input', 'button']]
*/
export type InitialInputData = (InitialInputs | null)[];
@ -499,10 +501,11 @@ export type InitialInputData = (InitialInputs | null)[];
* Used by InitialInputData to store input properties
* that should be set once from attributes.
*
* Even indices: minified/internal input name
* Odd indices: initial value
* i+0: attribute name
* i+1: minified/internal input name
* i+2: initial value
*
* e.g. ['role-min', 'button']
* e.g. ['role-min', 'minified-input', 'button']
*/
export type InitialInputs = string[];

View File

@ -8,7 +8,6 @@
import {InjectionToken} from '../../di/injection_token';
import {Injector} from '../../di/injector';
import {SimpleChanges} from '../../interface/simple_change';
import {Type} from '../../interface/type';
import {QueryList} from '../../linker';
import {Sanitizer} from '../../sanitization/security';