Skip to content

Commit c3967bd

Browse files
fix(ui-sref): Improve performance of generating hrefs
Closes angular-ui/ui-router#3361
1 parent bbe4209 commit c3967bd

File tree

4 files changed

+96
-65
lines changed

4 files changed

+96
-65
lines changed

src/params/param.ts

+43-16
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* @coreapi
33
* @module params
44
*/ /** for typedoc */
5-
import { extend, filter, map, applyPairs, allTrueR } from "../common/common";
6-
import { prop, propEq } from "../common/hof";
7-
import { isInjectable, isDefined, isString, isArray } from "../common/predicates";
5+
import { extend, filter, map, allTrueR } from "../common/common";
6+
import { prop } from "../common/hof";
7+
import { isInjectable, isDefined, isString, isArray, isUndefined } from "../common/predicates";
88
import { RawParams, ParamDeclaration } from "../params/interface";
99
import { services } from "../common/coreservices";
1010
import { ParamType } from "./paramType";
@@ -17,15 +17,22 @@ import { UrlMatcherFactory } from "../url/urlMatcherFactory";
1717

1818
/** @internalapi */
1919
export enum DefType {
20-
PATH, SEARCH, CONFIG
20+
PATH,
21+
SEARCH,
22+
CONFIG,
2123
}
2224

2325
/** @hidden */
2426
function unwrapShorthand(cfg: ParamDeclaration): ParamDeclaration {
2527
cfg = isShorthand(cfg) && { value: cfg } as any || cfg;
2628

29+
getStaticDefaultValue['__cacheable'] = true;
30+
function getStaticDefaultValue() {
31+
return cfg.value;
32+
}
33+
2734
return extend(cfg, {
28-
$$fn: isInjectable(cfg.value) ? cfg.value : () => cfg.value
35+
$$fn: isInjectable(cfg.value) ? cfg.value : getStaticDefaultValue,
2936
});
3037
}
3138

@@ -59,7 +66,7 @@ function getSquashPolicy(config: ParamDeclaration, isOptional: boolean, defaultP
5966
function getReplace(config: ParamDeclaration, arrayMode: boolean, isOptional: boolean, squash: (string|boolean)) {
6067
let replace: any, configuredKeys: string[], defaultPolicy = [
6168
{from: "", to: (isOptional || arrayMode ? undefined : "")},
62-
{from: null, to: (isOptional || arrayMode ? undefined : "")}
69+
{from: null, to: (isOptional || arrayMode ? undefined : "")},
6370
];
6471
replace = isArray(config.replace) ? config.replace : [];
6572
if (isString(squash)) replace.push({ from: squash, to: undefined });
@@ -77,10 +84,14 @@ export class Param {
7784
dynamic: boolean;
7885
raw: boolean;
7986
squash: (boolean|string);
80-
replace: any;
87+
replace: [{ to: any, from: any }];
8188
inherit: boolean;
8289
array: boolean;
8390
config: any;
91+
/** Cache the default value if it is a static value */
92+
_defaultValueCache: {
93+
defaultValue: any,
94+
};
8495

8596
constructor(id: string, type: ParamType, config: ParamDeclaration, location: DefType, urlMatcherFactory: UrlMatcherFactory) {
8697
config = unwrapShorthand(config);
@@ -101,7 +112,7 @@ export class Param {
101112
return extend(arrayDefaults, arrayParamNomenclature, config).array;
102113
}
103114

104-
extend(this, {id, type, location, isOptional, dynamic, raw, squash, replace, inherit, array: arrayMode, config, });
115+
extend(this, {id, type, location, isOptional, dynamic, raw, squash, replace, inherit, array: arrayMode, config });
105116
}
106117

107118
isDefaultValue(value: any): boolean {
@@ -116,21 +127,33 @@ export class Param {
116127
/**
117128
* [Internal] Get the default value of a parameter, which may be an injectable function.
118129
*/
119-
const $$getDefaultValue = () => {
130+
const getDefaultValue = () => {
131+
if (this._defaultValueCache) return this._defaultValueCache.defaultValue;
132+
120133
if (!services.$injector) throw new Error("Injectable functions cannot be called at configuration time");
134+
121135
let defaultValue = services.$injector.invoke(this.config.$$fn);
136+
122137
if (defaultValue !== null && defaultValue !== undefined && !this.type.is(defaultValue))
123138
throw new Error(`Default value (${defaultValue}) for parameter '${this.id}' is not an instance of ParamType (${this.type.name})`);
139+
140+
if (this.config.$$fn['__cacheable']) {
141+
this._defaultValueCache = { defaultValue };
142+
}
143+
124144
return defaultValue;
125145
};
126146

127-
const $replace = (val: any) => {
128-
let replacement: any = map(filter(this.replace, propEq('from', val)), prop("to"));
129-
return replacement.length ? replacement[0] : val;
147+
const replaceSpecialValues = (val: any) => {
148+
for (let tuple of this.replace) {
149+
if (tuple.from === val) return tuple.to;
150+
}
151+
return val;
130152
};
131153

132-
value = $replace(value);
133-
return !isDefined(value) ? $$getDefaultValue() : this.type.$normalize(value);
154+
value = replaceSpecialValues(value);
155+
156+
return isUndefined(value) ? getDefaultValue() : this.type.$normalize(value);
134157
}
135158

136159
isSearch(): boolean {
@@ -139,7 +162,7 @@ export class Param {
139162

140163
validates(value: any): boolean {
141164
// There was no parameter value, but the param is optional
142-
if ((!isDefined(value) || value === null) && this.isOptional) return true;
165+
if ((isUndefined(value) || value === null) && this.isOptional) return true;
143166

144167
// The value was not of the correct ParamType, and could not be decoded to the correct ParamType
145168
const normalized = this.type.$normalize(value);
@@ -155,7 +178,11 @@ export class Param {
155178
}
156179

157180
static values(params: Param[], values: RawParams = {}): RawParams {
158-
return <RawParams> params.map(param => [param.id, param.value(values[param.id])]).reduce(applyPairs, {});
181+
const paramValues = {} as RawParams;
182+
for (let param of params) {
183+
paramValues[param.id] = param.value(values[param.id]);
184+
}
185+
return paramValues;
159186
}
160187

161188
/**

src/state/stateService.ts

+36-39
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
/**
22
* @coreapi
33
* @module state
4-
*/ /** */
5-
import {
6-
extend, defaults, silentRejection, silenceUncaughtInPromise, removeFrom, noop, createProxyFunctions, inArray
7-
} from "../common/common";
8-
import {isDefined, isObject, isString} from "../common/predicates";
9-
import {Queue} from "../common/queue";
10-
import {services} from "../common/coreservices";
11-
12-
import {PathFactory} from "../path/pathFactory";
13-
import {PathNode} from "../path/node";
14-
15-
import {TransitionOptions, HookResult} from "../transition/interface";
16-
import {defaultTransOpts} from "../transition/transitionService";
17-
import {Rejection, RejectType} from "../transition/rejectFactory";
18-
import {Transition} from "../transition/transition";
19-
20-
import {StateOrName, StateDeclaration, TransitionPromise, LazyLoadResult} from "./interface";
21-
import {StateObject} from "./stateObject";
22-
import {TargetState} from "./targetState";
23-
24-
import {RawParams} from "../params/interface";
25-
import {ParamsOrArray} from "../params/interface";
26-
import {Param} from "../params/param";
27-
import {Glob} from "../common/glob";
28-
import {HrefOptions} from "./interface";
29-
import {UIRouter} from "../router";
30-
import {UIInjector} from "../interface";
31-
import {ResolveContext} from "../resolve/resolveContext";
32-
import {StateParams} from "../params/stateParams"; // has or is using
33-
import {lazyLoadState} from "../hooks/lazyLoad";
34-
import { val, not } from "../common/hof";
4+
*/
5+
/** */
6+
import { createProxyFunctions, defaults, extend, inArray, noop, removeFrom, silenceUncaughtInPromise, silentRejection } from '../common/common';
7+
import { isDefined, isObject, isString } from '../common/predicates';
8+
import { Queue } from '../common/queue';
9+
import { services } from '../common/coreservices';
10+
11+
import { PathFactory } from '../path/pathFactory';
12+
import { PathNode } from '../path/node';
13+
14+
import { HookResult, TransitionOptions } from '../transition/interface';
15+
import { defaultTransOpts } from '../transition/transitionService';
16+
import { Rejection, RejectType } from '../transition/rejectFactory';
17+
import { Transition } from '../transition/transition';
18+
19+
import { HrefOptions, LazyLoadResult, StateDeclaration, StateOrName, TransitionPromise } from './interface';
20+
import { StateObject } from './stateObject';
21+
import { TargetState } from './targetState';
22+
23+
import { ParamsOrArray, RawParams } from '../params/interface';
24+
import { Param } from '../params/param';
25+
import { Glob } from '../common/glob';
26+
import { UIRouter } from '../router';
27+
import { UIInjector } from '../interface';
28+
import { ResolveContext } from '../resolve/resolveContext';
29+
import { lazyLoadState } from '../hooks/lazyLoad';
30+
import { not, val } from '../common/hof';
31+
import { StateParams } from '../params/stateParams';
3532

3633
export type OnInvalidCallback =
3734
(toState?: TargetState, fromState?: TargetState, injector?: UIInjector) => HookResult;
@@ -51,25 +48,25 @@ export class StateService {
5148
*
5249
* This is a passthrough through to [[UIRouterGlobals.transition]]
5350
*/
54-
get transition() { return this.router.globals.transition; }
51+
get transition() { return this.router.globals.transition; }
5552
/**
5653
* The latest successful state parameters
5754
*
5855
* This is a passthrough through to [[UIRouterGlobals.params]]
5956
*/
60-
get params() { return this.router.globals.params; }
57+
get params(): StateParams { return this.router.globals.params; }
6158
/**
6259
* The current [[StateDeclaration]]
6360
*
6461
* This is a passthrough through to [[UIRouterGlobals.current]]
6562
*/
66-
get current() { return this.router.globals.current; }
63+
get current() { return this.router.globals.current; }
6764
/**
6865
* The current [[StateObject]]
6966
*
7067
* This is a passthrough through to [[UIRouterGlobals.$current]]
7168
*/
72-
get $current() { return this.router.globals.$current; }
69+
get $current() { return this.router.globals.$current; }
7370

7471
/** @internalapi */
7572
constructor(private router: UIRouter) {
@@ -208,7 +205,7 @@ export class StateService {
208205
return this.transitionTo(this.current, this.params, {
209206
reload: isDefined(reloadState) ? reloadState : true,
210207
inherit: false,
211-
notify: false
208+
notify: false,
212209
});
213210
};
214211

@@ -499,7 +496,7 @@ export class StateService {
499496
lossy: true,
500497
inherit: true,
501498
absolute: false,
502-
relative: this.$current
499+
relative: this.$current,
503500
};
504501
options = defaults(options, defaultHrefOpts);
505502
params = params || {};
@@ -514,8 +511,8 @@ export class StateService {
514511
if (!nav || nav.url === undefined || nav.url === null) {
515512
return null;
516513
}
517-
return this.router.urlRouter.href(nav.url, Param.values(state.parameters(), params), {
518-
absolute: options.absolute
514+
return this.router.urlRouter.href(nav.url, params, {
515+
absolute: options.absolute,
519516
});
520517
};
521518

src/url/urlMatcher.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ export class UrlMatcher {
333333
*/
334334
parameters(opts: any = {}): Param[] {
335335
if (opts.inherit === false) return this._params;
336-
return unnest(this._cache.path.map(prop('_params')));
336+
return unnest(this._cache.path.map(matcher => matcher._params));
337337
}
338338

339339
/**
@@ -345,12 +345,14 @@ export class UrlMatcher {
345345
* @returns {T|Param|any|boolean|UrlMatcher|null}
346346
*/
347347
parameter(id: string, opts: any = {}): Param {
348+
const findParam = () => {
349+
for (let param of this._params) {
350+
if (param.id === id) return param;
351+
}
352+
};
353+
348354
let parent = this._cache.parent;
349-
return (
350-
find(this._params, propEq('id', id)) ||
351-
(opts.inherit !== false && parent && parent.parameter(id, opts)) ||
352-
null
353-
);
355+
return findParam() || (opts.inherit !== false && parent && parent.parameter(id, opts)) || null;
354356
}
355357

356358
/**
@@ -363,9 +365,14 @@ export class UrlMatcher {
363365
* @returns Returns `true` if `params` validates, otherwise `false`.
364366
*/
365367
validates(params: RawParams): boolean {
366-
const validParamVal = (param: Param, val: any) =>
368+
const validParamVal = (param: Param, val: any) =>
367369
!param || param.validates(val);
368-
return pairs(params || {}).map(([key, val]) => validParamVal(this.parameter(key), val)).reduce(allTrueR, true);
370+
371+
params = params || {};
372+
373+
// I'm not sure why this checks only the param keys passed in, and not all the params known to the matcher
374+
let paramSchema = this.parameters().filter(paramDef => params.hasOwnProperty(paramDef.id));
375+
return paramSchema.map(paramDef => validParamVal(paramDef, params[paramDef.id])).reduce(allTrueR, true);
369376
}
370377

371378
/**

src/url/urlRouter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
214214
* @returns Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher`
215215
*/
216216
href(urlMatcher: UrlMatcher, params?: any, options?: { absolute: boolean }): string {
217-
if (!urlMatcher.validates(params)) return null;
218-
219217
let url = urlMatcher.format(params);
218+
if (url == null) return null;
219+
220220
options = options || { absolute: false };
221221

222222
let cfg = this._router.urlService.config;

0 commit comments

Comments
 (0)