Victor Berchet 3bcc0e6f76 refactor(core): refactor WrappedValue (#20997)
- Improve `WrappedValue` by adding `unwrap` symetrical to `wrap`.
- remove dead code - `ValueUnwrapper`

The property `wrapped` is an implementation details and should never be accessed
directly - use `unwrap(wrappedValue)`. Will change to protected in Angular 7.

PR Close #20997
2018-01-16 07:12:58 -08:00

562 lines
21 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {ChangeDetectorRef, SimpleChange, SimpleChanges, WrappedValue} from '../change_detection/change_detection';
import {Injector, resolveForwardRef} from '../di';
import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer as RendererV1, Renderer2} from '../render/api';
import {stringify} from '../util';
import {createChangeDetectorRef, createInjector, createRendererV1} from './refs';
import {BindingDef, BindingFlags, DepDef, DepFlags, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryValueType, Services, ViewData, ViewFlags, ViewState, asElementData, asProviderData, shouldCallLifecycleInitHook} from './types';
import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util';
const RendererV1TokenKey = tokenKey(RendererV1);
const Renderer2TokenKey = tokenKey(Renderer2);
const ElementRefTokenKey = tokenKey(ElementRef);
const ViewContainerRefTokenKey = tokenKey(ViewContainerRef);
const TemplateRefTokenKey = tokenKey(TemplateRef);
const ChangeDetectorRefTokenKey = tokenKey(ChangeDetectorRef);
const InjectorRefTokenKey = tokenKey(Injector);
export function directiveDef(
checkIndex: number, flags: NodeFlags,
matchedQueries: null | [string | number, QueryValueType][], childCount: number, ctor: any,
deps: ([DepFlags, any] | any)[], props?: null | {[name: string]: [number, string]},
outputs?: null | {[name: string]: string}): NodeDef {
const bindings: BindingDef[] = [];
if (props) {
for (let prop in props) {
const [bindingIndex, nonMinifiedName] = props[prop];
bindings[bindingIndex] = {
flags: BindingFlags.TypeProperty,
name: prop, nonMinifiedName,
ns: null,
securityContext: null,
suffix: null
};
}
}
const outputDefs: OutputDef[] = [];
if (outputs) {
for (let propName in outputs) {
outputDefs.push(
{type: OutputType.DirectiveOutput, propName, target: null, eventName: outputs[propName]});
}
}
flags |= NodeFlags.TypeDirective;
return _def(
checkIndex, flags, matchedQueries, childCount, ctor, ctor, deps, bindings, outputDefs);
}
export function pipeDef(flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[]): NodeDef {
flags |= NodeFlags.TypePipe;
return _def(-1, flags, null, 0, ctor, ctor, deps);
}
export function providerDef(
flags: NodeFlags, matchedQueries: null | [string | number, QueryValueType][], token: any,
value: any, deps: ([DepFlags, any] | any)[]): NodeDef {
return _def(-1, flags, matchedQueries, 0, token, value, deps);
}
export function _def(
checkIndex: number, flags: NodeFlags,
matchedQueriesDsl: [string | number, QueryValueType][] | null, childCount: number, token: any,
value: any, deps: ([DepFlags, any] | any)[], bindings?: BindingDef[],
outputs?: OutputDef[]): NodeDef {
const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl);
if (!outputs) {
outputs = [];
}
if (!bindings) {
bindings = [];
}
// Need to resolve forwardRefs as e.g. for `useValue` we
// lowered the expression and then stopped evaluating it,
// i.e. also didn't unwrap it.
value = resolveForwardRef(value);
const depDefs = splitDepsDsl(deps, stringify(token));
return {
// will bet set by the view definition
nodeIndex: -1,
parent: null,
renderParent: null,
bindingIndex: -1,
outputIndex: -1,
// regular values
checkIndex,
flags,
childFlags: 0,
directChildFlags: 0,
childMatchedQueries: 0, matchedQueries, matchedQueryIds, references,
ngContentIndex: -1, childCount, bindings,
bindingFlags: calcBindingFlags(bindings), outputs,
element: null,
provider: {token, value, deps: depDefs},
text: null,
query: null,
ngContent: null
};
}
export function createProviderInstance(view: ViewData, def: NodeDef): any {
return _createProviderInstance(view, def);
}
export function createPipeInstance(view: ViewData, def: NodeDef): any {
// deps are looked up from component.
let compView = view;
while (compView.parent && !isComponentView(compView)) {
compView = compView.parent;
}
// pipes can see the private services of the component
const allowPrivateServices = true;
// pipes are always eager and classes!
return createClass(
compView.parent !, viewParentEl(compView) !, allowPrivateServices, def.provider !.value,
def.provider !.deps);
}
export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
// components can see other private services, other directives can't.
const allowPrivateServices = (def.flags & NodeFlags.Component) > 0;
// directives are always eager and classes!
const instance = createClass(
view, def.parent !, allowPrivateServices, def.provider !.value, def.provider !.deps);
if (def.outputs.length) {
for (let i = 0; i < def.outputs.length; i++) {
const output = def.outputs[i];
const subscription = instance[output.propName !].subscribe(
eventHandlerClosure(view, def.parent !.nodeIndex, output.eventName));
view.disposables ![def.outputIndex + i] = subscription.unsubscribe.bind(subscription);
}
}
return instance;
}
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event);
}
export function checkAndUpdateDirectiveInline(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
v7: any, v8: any, v9: any): boolean {
const providerData = asProviderData(view, def.nodeIndex);
const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
const bindLen = def.bindings.length;
if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
changed = true;
changes = updateProp(view, providerData, def, 0, v0, changes);
}
if (bindLen > 1 && checkBinding(view, def, 1, v1)) {
changed = true;
changes = updateProp(view, providerData, def, 1, v1, changes);
}
if (bindLen > 2 && checkBinding(view, def, 2, v2)) {
changed = true;
changes = updateProp(view, providerData, def, 2, v2, changes);
}
if (bindLen > 3 && checkBinding(view, def, 3, v3)) {
changed = true;
changes = updateProp(view, providerData, def, 3, v3, changes);
}
if (bindLen > 4 && checkBinding(view, def, 4, v4)) {
changed = true;
changes = updateProp(view, providerData, def, 4, v4, changes);
}
if (bindLen > 5 && checkBinding(view, def, 5, v5)) {
changed = true;
changes = updateProp(view, providerData, def, 5, v5, changes);
}
if (bindLen > 6 && checkBinding(view, def, 6, v6)) {
changed = true;
changes = updateProp(view, providerData, def, 6, v6, changes);
}
if (bindLen > 7 && checkBinding(view, def, 7, v7)) {
changed = true;
changes = updateProp(view, providerData, def, 7, v7, changes);
}
if (bindLen > 8 && checkBinding(view, def, 8, v8)) {
changed = true;
changes = updateProp(view, providerData, def, 8, v8, changes);
}
if (bindLen > 9 && checkBinding(view, def, 9, v9)) {
changed = true;
changes = updateProp(view, providerData, def, 9, v9, changes);
}
if (changes) {
directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
return changed;
}
export function checkAndUpdateDirectiveDynamic(
view: ViewData, def: NodeDef, values: any[]): boolean {
const providerData = asProviderData(view, def.nodeIndex);
const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
for (let i = 0; i < values.length; i++) {
if (checkBinding(view, def, i, values[i])) {
changed = true;
changes = updateProp(view, providerData, def, i, values[i], changes);
}
}
if (changes) {
directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
return changed;
}
function _createProviderInstance(view: ViewData, def: NodeDef): any {
// private services can see other private services
const allowPrivateServices = (def.flags & NodeFlags.PrivateProvider) > 0;
const providerDef = def.provider;
switch (def.flags & NodeFlags.Types) {
case NodeFlags.TypeClassProvider:
return createClass(
view, def.parent !, allowPrivateServices, providerDef !.value, providerDef !.deps);
case NodeFlags.TypeFactoryProvider:
return callFactory(
view, def.parent !, allowPrivateServices, providerDef !.value, providerDef !.deps);
case NodeFlags.TypeUseExistingProvider:
return resolveDep(view, def.parent !, allowPrivateServices, providerDef !.deps[0]);
case NodeFlags.TypeValueProvider:
return providerDef !.value;
}
}
function createClass(
view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, ctor: any, deps: DepDef[]): any {
const len = deps.length;
switch (len) {
case 0:
return new ctor();
case 1:
return new ctor(resolveDep(view, elDef, allowPrivateServices, deps[0]));
case 2:
return new ctor(
resolveDep(view, elDef, allowPrivateServices, deps[0]),
resolveDep(view, elDef, allowPrivateServices, deps[1]));
case 3:
return new ctor(
resolveDep(view, elDef, allowPrivateServices, deps[0]),
resolveDep(view, elDef, allowPrivateServices, deps[1]),
resolveDep(view, elDef, allowPrivateServices, deps[2]));
default:
const depValues = new Array(len);
for (let i = 0; i < len; i++) {
depValues[i] = resolveDep(view, elDef, allowPrivateServices, deps[i]);
}
return new ctor(...depValues);
}
}
function callFactory(
view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, factory: any,
deps: DepDef[]): any {
const len = deps.length;
switch (len) {
case 0:
return factory();
case 1:
return factory(resolveDep(view, elDef, allowPrivateServices, deps[0]));
case 2:
return factory(
resolveDep(view, elDef, allowPrivateServices, deps[0]),
resolveDep(view, elDef, allowPrivateServices, deps[1]));
case 3:
return factory(
resolveDep(view, elDef, allowPrivateServices, deps[0]),
resolveDep(view, elDef, allowPrivateServices, deps[1]),
resolveDep(view, elDef, allowPrivateServices, deps[2]));
default:
const depValues = Array(len);
for (let i = 0; i < len; i++) {
depValues[i] = resolveDep(view, elDef, allowPrivateServices, deps[i]);
}
return factory(...depValues);
}
}
// This default value is when checking the hierarchy for a token.
//
// It means both:
// - the token is not provided by the current injector,
// - only the element injectors should be checked (ie do not check module injectors
//
// mod1
// /
// el1 mod2
// \ /
// el2
//
// When requesting el2.injector.get(token), we should check in the following order and return the
// first found value:
// - el2.injector.get(token, default)
// - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module
// - mod2.injector.get(token, default)
export const NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR = {};
export function resolveDep(
view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef,
notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
if (depDef.flags & DepFlags.Value) {
return depDef.token;
}
const startView = view;
if (depDef.flags & DepFlags.Optional) {
notFoundValue = null;
}
const tokenKey = depDef.tokenKey;
if (tokenKey === ChangeDetectorRefTokenKey) {
// directives on the same element as a component should be able to control the change detector
// of that component as well.
allowPrivateServices = !!(elDef && elDef.element !.componentView);
}
if (elDef && (depDef.flags & DepFlags.SkipSelf)) {
allowPrivateServices = false;
elDef = elDef.parent !;
}
while (view) {
if (elDef) {
switch (tokenKey) {
case RendererV1TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices);
return createRendererV1(compView);
}
case Renderer2TokenKey: {
const compView = findCompView(view, elDef, allowPrivateServices);
return compView.renderer;
}
case ElementRefTokenKey:
return new ElementRef(asElementData(view, elDef.nodeIndex).renderElement);
case ViewContainerRefTokenKey:
return asElementData(view, elDef.nodeIndex).viewContainer;
case TemplateRefTokenKey: {
if (elDef.element !.template) {
return asElementData(view, elDef.nodeIndex).template;
}
break;
}
case ChangeDetectorRefTokenKey: {
let cdView = findCompView(view, elDef, allowPrivateServices);
return createChangeDetectorRef(cdView);
}
case InjectorRefTokenKey:
return createInjector(view, elDef);
default:
const providerDef =
(allowPrivateServices ? elDef.element !.allProviders :
elDef.element !.publicProviders) ![tokenKey];
if (providerDef) {
let providerData = asProviderData(view, providerDef.nodeIndex);
if (!providerData) {
providerData = {instance: _createProviderInstance(view, providerDef)};
view.nodes[providerDef.nodeIndex] = providerData as any;
}
return providerData.instance;
}
}
}
allowPrivateServices = isComponentView(view);
elDef = viewParentEl(view) !;
view = view.parent !;
}
const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
// Return the value from the root element injector when
// - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value;
}
return startView.root.ngModule.injector.get(depDef.token, notFoundValue);
}
function findCompView(view: ViewData, elDef: NodeDef, allowPrivateServices: boolean) {
let compView: ViewData;
if (allowPrivateServices) {
compView = asElementData(view, elDef.nodeIndex).componentView;
} else {
compView = view;
while (compView.parent && !isComponentView(compView)) {
compView = compView.parent;
}
}
return compView;
}
function updateProp(
view: ViewData, providerData: ProviderData, def: NodeDef, bindingIdx: number, value: any,
changes: SimpleChanges): SimpleChanges {
if (def.flags & NodeFlags.Component) {
const compView = asElementData(view, def.parent !.nodeIndex).componentView;
if (compView.def.flags & ViewFlags.OnPush) {
compView.state |= ViewState.ChecksEnabled;
}
}
const binding = def.bindings[bindingIdx];
const propName = binding.name !;
// Note: This is still safe with Closure Compiler as
// the user passed in the property name as an object has to `providerDef`,
// so Closure Compiler will have renamed the property correctly already.
providerData.instance[propName] = value;
if (def.flags & NodeFlags.OnChanges) {
changes = changes || {};
const oldValue = WrappedValue.unwrap(view.oldValues[def.bindingIndex + bindingIdx]);
const binding = def.bindings[bindingIdx];
changes[binding.nonMinifiedName !] =
new SimpleChange(oldValue, value, (view.state & ViewState.FirstCheck) !== 0);
}
view.oldValues[def.bindingIndex + bindingIdx] = value;
return changes;
}
// This function calls the ngAfterContentCheck, ngAfterContentInit,
// ngAfterViewCheck, and ngAfterViewInit lifecycle hooks (depending on the node
// flags in lifecycle). Unlike ngDoCheck, ngOnChanges and ngOnInit, which are
// called during a pre-order traversal of the view tree (that is calling the
// parent hooks before the child hooks) these events are sent in using a
// post-order traversal of the tree (children before parents). This changes the
// meaning of initIndex in the view state. For ngOnInit, initIndex tracks the
// expected nodeIndex which a ngOnInit should be called. When sending
// ngAfterContentInit and ngAfterViewInit it is the expected count of
// ngAfterContentInit or ngAfterViewInit methods that have been called. This
// ensure that dispite being called recursively or after picking up after an
// exception, the ngAfterContentInit or ngAfterViewInit will be called on the
// correct nodes. Consider for example, the following (where E is an element
// and D is a directive)
// Tree: pre-order index post-order index
// E1 0 6
// E2 1 1
// D3 2 0
// E4 3 5
// E5 4 4
// E6 5 2
// E7 6 3
// As can be seen, the post-order index has an unclear relationship to the
// pre-order index (postOrderIndex === preOrderIndex - parentCount +
// childCount). Since number of calls to ngAfterContentInit and ngAfterViewInit
// are stable (will be the same for the same view regardless of exceptions or
// recursion) we just need to count them which will roughly correspond to the
// post-order index (it skips elements and directives that do not have
// lifecycle hooks).
//
// For example, if an exception is raised in the E6.onAfterViewInit() the
// initIndex is left at 3 (by shouldCallLifecycleInitHook() which set it to
// initIndex + 1). When checkAndUpdateView() is called again D3, E2 and E6 will
// not have their ngAfterViewInit() called but, starting with E7, the rest of
// the view will begin getting ngAfterViewInit() called until a check and
// pass is complete.
//
// This algorthim also handles recursion. Consider if E4's ngAfterViewInit()
// indirectly calls E1's ChangeDetectorRef.detectChanges(). The expected
// initIndex is set to 6, the recusive checkAndUpdateView() starts walk again.
// D3, E2, E6, E7, E5 and E4 are skipped, ngAfterViewInit() is called on E1.
// When the recursion returns the initIndex will be 7 so E1 is skipped as it
// has already been called in the recursively called checkAnUpdateView().
export function callLifecycleHooksChildrenFirst(view: ViewData, lifecycles: NodeFlags) {
if (!(view.def.nodeFlags & lifecycles)) {
return;
}
const nodes = view.def.nodes;
let initIndex = 0;
for (let i = 0; i < nodes.length; i++) {
const nodeDef = nodes[i];
let parent = nodeDef.parent;
if (!parent && nodeDef.flags & lifecycles) {
// matching root node (e.g. a pipe)
callProviderLifecycles(view, i, nodeDef.flags & lifecycles, initIndex++);
}
if ((nodeDef.childFlags & lifecycles) === 0) {
// no child matches one of the lifecycles
i += nodeDef.childCount;
}
while (parent && (parent.flags & NodeFlags.TypeElement) &&
i === parent.nodeIndex + parent.childCount) {
// last child of an element
if (parent.directChildFlags & lifecycles) {
initIndex = callElementProvidersLifecycles(view, parent, lifecycles, initIndex);
}
parent = parent.parent;
}
}
}
function callElementProvidersLifecycles(
view: ViewData, elDef: NodeDef, lifecycles: NodeFlags, initIndex: number): number {
for (let i = elDef.nodeIndex + 1; i <= elDef.nodeIndex + elDef.childCount; i++) {
const nodeDef = view.def.nodes[i];
if (nodeDef.flags & lifecycles) {
callProviderLifecycles(view, i, nodeDef.flags & lifecycles, initIndex++);
}
// only visit direct children
i += nodeDef.childCount;
}
return initIndex;
}
function callProviderLifecycles(
view: ViewData, index: number, lifecycles: NodeFlags, initIndex: number) {
const providerData = asProviderData(view, index);
if (!providerData) {
return;
}
const provider = providerData.instance;
if (!provider) {
return;
}
Services.setCurrentNode(view, index);
if (lifecycles & NodeFlags.AfterContentInit &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingAfterContentInit, initIndex)) {
provider.ngAfterContentInit();
}
if (lifecycles & NodeFlags.AfterContentChecked) {
provider.ngAfterContentChecked();
}
if (lifecycles & NodeFlags.AfterViewInit &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingAfterViewInit, initIndex)) {
provider.ngAfterViewInit();
}
if (lifecycles & NodeFlags.AfterViewChecked) {
provider.ngAfterViewChecked();
}
if (lifecycles & NodeFlags.OnDestroy) {
provider.ngOnDestroy();
}
}