Skip to content

Commit b065d14

Browse files
committed
Cleanup, add tests.
* remove old implementation * add API docs * rename some API * add dynamicFn to dep count * add test for method as dependency
1 parent 7cda770 commit b065d14

File tree

5 files changed

+207
-214
lines changed

5 files changed

+207
-214
lines changed

lib/mixins/property-effects.js

Lines changed: 94 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -415,45 +415,59 @@ function runReflectEffect(inst, property, props, oldProps, info) {
415415
function runComputedEffects(inst, changedProps, oldProps, hasPaths) {
416416
let computeEffects = inst[TYPES.COMPUTE];
417417
if (computeEffects) {
418-
if (orderedComputed == 2) {
418+
if (orderedComputed) {
419+
// Runs computed effects in efficient order by keeping a topologically-
420+
// sorted queue of compute effects to run, and inserting subsequently
421+
// invalidated effects as they are run
419422
dedupeId++;
420-
const order = orderedComputedDeps(inst);
423+
const order = getComputedOrder(inst);
421424
const queue = [];
422425
for (let p in changedProps) {
423-
addEffectsFor(p, computeEffects, queue, order, hasPaths);
426+
enqueueEffectsFor(p, computeEffects, queue, order, hasPaths);
424427
}
425428
let info;
426429
while ((info = queue.shift())) {
427430
if (runComputedEffect(inst, '', changedProps, oldProps, info, hasPaths)) {
428-
addEffectsFor(info.methodInfo, computeEffects, queue, order, hasPaths);
431+
enqueueEffectsFor(info.methodInfo, computeEffects, queue, order, hasPaths);
429432
}
430433
}
431434
Object.assign(/** @type {!Object} */ (oldProps), inst.__dataOld);
432435
Object.assign(/** @type {!Object} */ (changedProps), inst.__dataPending);
433436
inst.__dataPending = null;
434437
} else {
438+
// Original Polymer 2.x computed effects order, which continues running
439+
// effects until no further computed properties have been invalidated
435440
let inputProps = changedProps;
436441
while (runEffects(inst, computeEffects, inputProps, oldProps, hasPaths)) {
437442
Object.assign(/** @type {!Object} */ (oldProps), inst.__dataOld);
438443
Object.assign(/** @type {!Object} */ (changedProps), inst.__dataPending);
439444
inputProps = inst.__dataPending;
440445
inst.__dataPending = null;
441-
if (orderedComputed) {
442-
// Ensure all computed properties are de-duped against the same turn
443-
dedupeId--;
444-
}
445446
}
446447
}
447448
}
448449
}
449450

450-
const insertEffect = (info, effects, order) => {
451+
/**
452+
* Inserts a computed effect into a queue, given the specified order. Performs
453+
* the insert using a binary search.
454+
*
455+
* Used by `orderedComputed: true` computed property algorithm.
456+
*
457+
* @param {Object} info Property effects metadata
458+
* @param {Array<Object>} queue Ordered queue of effects
459+
* @param {Map<string,number>} order Map of computed property name->topological
460+
* sort order
461+
*/
462+
const insertEffect = (info, queue, order) => {
451463
let start = 0;
452-
let end = effects.length - 1;
464+
let end = queue.length - 1;
453465
let idx = -1;
454466
while (start <= end) {
455467
const mid = (start + end) >> 1;
456-
const cmp = order.get(effects[mid].methodInfo) - order.get(info.methodInfo);
468+
// Note `methodInfo` is where the computed property name is stored in
469+
// the effect metadata
470+
const cmp = order.get(queue[mid].methodInfo) - order.get(info.methodInfo);
457471
if (cmp < 0) {
458472
start = mid + 1;
459473
} else if (cmp > 0) {
@@ -466,10 +480,24 @@ const insertEffect = (info, effects, order) => {
466480
if (idx < 0) {
467481
idx = end + 1;
468482
}
469-
effects.splice(idx, 0, info);
483+
queue.splice(idx, 0, info);
470484
};
471485

472-
const addEffectsFor = (prop, computeEffects, queue, order, hasPaths) => {
486+
/**
487+
* Inserts all downstream computed effects invalidated by the specified property
488+
* into the topologically-sorted queue of effects to be run.
489+
*
490+
* Used by `orderedComputed: true` computed property algorithm.
491+
*
492+
* @param {string} prop Property name
493+
* @param {Object} computeEffects Computed effects for this element
494+
* @param {Array<Obbject>} queue Topologically-sorted queue of computed effects
495+
* to be run
496+
* @param {Map<string,numer>} order Map of computed property name->topological
497+
* sort order
498+
* @param {boolean} hasPaths True with `changedProps` contains one or more paths
499+
*/
500+
const enqueueEffectsFor = (prop, computeEffects, queue, order, hasPaths) => {
473501
const rootProperty = hasPaths ? root(prop) : prop;
474502
const fxs = computeEffects[rootProperty];
475503
if (fxs) {
@@ -484,21 +512,43 @@ const addEffectsFor = (prop, computeEffects, queue, order, hasPaths) => {
484512
}
485513
};
486514

487-
function orderedComputedDeps(inst) {
515+
/**
516+
* Generates and retrieves a memoized map of computed property name to its
517+
* topologically-sorted order.
518+
*
519+
* The map is generated by first assigning a "dependency count" to each property
520+
* (defined as number properties it depends on, including its method for
521+
* "dynamic functions"). Any properties that have no dependencies are added to
522+
* the `ready` queue, which are properties whose order can be added to the final
523+
* order map. Properties are popped off the `ready` one by one and a.) added as
524+
* the next property in the order map, and b.) each property that it is a
525+
* dependency for has its dep count decremented (and if that property's dep
526+
* count goes to zero, it is added to the `ready` queue), until all properties
527+
* have been visited and ordered.
528+
*
529+
* Used by `orderedComputed: true` computed property algorithm.
530+
*
531+
* @param {!Polymer_PropertyEffects} inst The instance to retrieve the computed
532+
* effect order for.
533+
* @return {Map<string,numbber>} Map of computed property name->topological sort
534+
* order
535+
*/
536+
function getComputedOrder(inst) {
488537
let ordered = inst.constructor.__orderedComputedDeps;
489538
if (!ordered) {
490539
ordered = new Map();
491540
const effects = inst[TYPES.COMPUTE];
492-
const {counts, next} = edgeCounts(inst);
541+
const {counts, ready} = dependencyCounts(inst);
493542
let curr;
494-
while ((curr = next.pop())) {
543+
while ((curr = ready.shift())) {
495544
ordered.set(curr, ordered.size);
496545
const computedByCurr = effects[curr];
497546
if (computedByCurr) {
498547
computedByCurr.forEach(fx => {
499-
const p = fx.info.methodInfo;
500-
if (--counts[p] === 0) {
501-
next.push(p);
548+
// Note `methodInfo` is where the computed property name is stored
549+
const computedProp = fx.info.methodInfo;
550+
if (--counts[computedProp] === 0) {
551+
ready.push(computedProp);
502552
}
503553
});
504554
}
@@ -508,24 +558,37 @@ function orderedComputedDeps(inst) {
508558
return ordered;
509559
}
510560

511-
function edgeCounts(inst) {
561+
/**
562+
* Generates a map of property-to-dependency count (`counts`, where "dependency
563+
* count" is the number of dependencies a given property has assuming it is a
564+
* computed property, otherwise 0). It also returns a pre-populated list of
565+
* `ready` properties that have no dependencies.
566+
*
567+
* Used by `orderedComputed: true` computed property algorithm.
568+
*
569+
* @param {!Polymer_PropertyEffects} inst The instance to generate dependency
570+
* counts for.
571+
* @return {Object} Object containing `counts` map (property-to-dependency
572+
* count) and pre-populated `ready` array of properties that had zero
573+
* dependencies.
574+
*/
575+
function dependencyCounts(inst) {
512576
const props = inst.constructor._properties;
513-
const depsForComputed = inst[COMPUTE_INFO];
577+
const infoForComputed = inst[COMPUTE_INFO];
514578
const counts = {};
515-
const next = [];
579+
const ready = [];
516580
for (let p in props) {
517-
const deps = depsForComputed[p];
518-
if (deps) {
519-
counts[p] = deps.args.length;
581+
const info = infoForComputed[p];
582+
if (info) {
583+
// Be sure to add the method name itself in case of "dynamic functions"
584+
counts[p] = info.args.length + (info.dynamicFn ? 1 : 0);
520585
} else {
521-
next.push(p);
586+
ready.push(p);
522587
}
523588
}
524-
return {counts, next};
589+
return {counts, ready};
525590
}
526591

527-
const TRANSITIVE_DEPENDENCY = '~transitive~dependency~';
528-
529592
/**
530593
* Implements the "computed property" effect by running the method with the
531594
* values of the arguments specified in the `info` object and setting the
@@ -540,120 +603,19 @@ const TRANSITIVE_DEPENDENCY = '~transitive~dependency~';
540603
* @return {boolean} True when the property being computed changed
541604
* @private
542605
*/
543-
function runComputedEffect(inst, property, changedProps, oldProps, info, hasPaths) {
544-
if (orderedComputed == 1) {
545-
// Compute any computed dependencies first; this recurses through the
546-
// dependency graph to ensure computed properties are never computed with
547-
// dependencies that may become invalidated later in this turn
548-
computeDependencies(inst, info, changedProps, oldProps, hasPaths);
549-
// If the dependency was not transitive, it's definitely dirty and needs to
550-
// be computed; if it is transitive, check its arguments against the current
551-
// changed props and only re-compute if it is dirty
552-
if (property === TRANSITIVE_DEPENDENCY &&
553-
!computedPropertyIsDirty(inst, info, changedProps, hasPaths)) {
554-
return;
555-
}
556-
}
606+
function runComputedEffect(inst, property, changedProps, oldProps, info) {
557607
// Dirty check dependencies and run if any invalid
558608
let result = runMethodEffect(inst, property, changedProps, oldProps, info);
559609
// Abort if method returns a no-op result
560610
if (result === NOOP) {
561-
return;
611+
return false;
562612
}
563613
let computedProp = info.methodInfo;
564614
if (inst.__dataHasAccessor && inst.__dataHasAccessor[computedProp]) {
565615
return inst._setPendingProperty(computedProp, result, true);
566616
} else {
567617
inst[computedProp] = result;
568-
}
569-
}
570-
571-
/**
572-
* Returns any dependencies of a computed property that are themselves
573-
* also computed.
574-
*
575-
* @param {!Polymer_PropertyEffects} inst The instance to test
576-
* @param {?} info Computed effect metadata
577-
* @return {Array<?>} Array of computed effect metadata for depenencies
578-
* @private
579-
*/
580-
function computedDependenciesFor(inst, info) {
581-
let deps = info.computedDependencies;
582-
if (!deps) {
583-
deps = info.computedDependencies = [];
584-
info.args.forEach(arg => {
585-
const dep = arg.rootProperty || arg.name;
586-
const depInfo = inst[COMPUTE_INFO][dep];
587-
if (depInfo) {
588-
deps.push(depInfo);
589-
}
590-
});
591-
}
592-
return deps;
593-
}
594-
595-
/**
596-
* Runs computed effects for dependencies of the computed property effect
597-
* described by `info`.
598-
*
599-
* @param {!Polymer_PropertyEffects} inst The instance to test
600-
* @param {Object} info Effect metadata
601-
* @param {Object} changedProps Bag of current property changes
602-
* @param {Object} oldProps Bag of previous values for changed properties
603-
* @param {boolean} hasPaths True with `changedProps` contains one or more paths
604-
* @return {void}
605-
* @private
606-
*/
607-
function computeDependencies(inst, info, changedProps, oldProps, hasPaths) {
608-
let deps = computedDependenciesFor(inst, info);
609-
if (deps.length) {
610-
deps.forEach(depInfo => {
611-
if (depInfo.lastRun !== info.lastRun) {
612-
depInfo.lastRun = info.lastRun;
613-
runComputedEffect(inst, TRANSITIVE_DEPENDENCY, changedProps, oldProps, depInfo, hasPaths);
614-
}
615-
});
616-
}
617-
}
618-
619-
/**
620-
* Returns whether the computed effect has any changed dependencies.
621-
*
622-
* @param {!Polymer_PropertyEffects} inst The instance to test
623-
* @param {?} info Effect metadata
624-
* @param {Object} changedProps Bag of current property changes
625-
* @param {boolean} hasPaths True with `changedProps` contains one or more paths
626-
* @return {boolean} true when the computed effect should be re-calculated
627-
* @private
628-
*/
629-
function computedPropertyIsDirty(inst, info, changedProps, hasPaths) {
630-
return info.dynamicFn && propertyIsDirty(inst, {rootProperty: info.methodName}, changedProps, hasPaths) ||
631-
info.args.some(arg => propertyIsDirty(inst, arg, changedProps, hasPaths));
632-
}
633-
634-
/**
635-
* Returns whether any changed props match the effect trigger
636-
*
637-
* @param {!Polymer_PropertyEffects} inst The instance to test
638-
* @param {DataTrigger} trigger Descriptor
639-
* @param {Object} changedProps Bag of current property changes
640-
* @param {boolean} hasPaths True with `changedProps` contains one or more paths
641-
* @return {boolean} true when the property is dirty
642-
* @private
643-
*/
644-
function propertyIsDirty(inst, trigger, changedProps, hasPaths) {
645-
if (hasPaths) {
646-
[changedProps, inst.__dataPending].some(props => {
647-
for (let p in props) {
648-
if (pathMatchesTrigger(p, trigger)) {
649-
return true;
650-
}
651-
}
652-
});
653618
return false;
654-
} else {
655-
return Boolean(changedProps && trigger.rootProperty in changedProps ||
656-
inst.__dataPending && trigger.rootProperty in inst.__dataPending);
657619
}
658620
}
659621

test/runner.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
'unit/property-effects.html',
3434
'unit/property-effects.html?legacyUndefined=true',
3535
'unit/property-effects.html?legacyUndefined=true&legacyNoBatch=true',
36+
'unit/property-effects.html?orderedComputed=true',
3637
'unit/property-effects-template.html',
3738
'unit/path-effects.html',
39+
'unit/path-effects.html?orderedComputed=true',
3840
'unit/shady.html',
3941
'unit/shady-events.html',
4042
'unit/shady-content.html',

test/unit/path-effects.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<script type="module">
2222
import './path-effects-elements.js';
2323
import { Polymer } from '../../lib/legacy/polymer-fn.js';
24+
import { setOrderedComputed } from '../../lib/utils/settings.js';
25+
26+
setOrderedComputed(Boolean(window.location.search.match('orderedComputed')));
2427

2528
// Safari 9 has a horrible bug related to array enumeration after defining
2629
// a non-enumerable property on it (we do for `.splices`); for now we work

test/unit/property-effects-elements.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,16 +1014,22 @@ customElements.define(SubObserverElement.is, SubObserverElement);
10141014
customElements.define('x-computed-ordering', class extends PolymerElement {
10151015
static get properties() {
10161016
return {
1017-
a: {type: Number},
1018-
b: {type: Number},
1019-
c: {type: Number},
1020-
d: {type: Number},
1017+
a: {type: Number, value: 1000},
1018+
b: {type: Number, value: 100},
1019+
c: {type: Number, value: 10},
1020+
d: {type: Number, value: 1},
10211021
abbcd: {computed: 'computeABBCD(a, b, bcd)', observer: 'abbcdChanged'},
10221022
bcd: {computed: 'computeBCD(bc, d)', observer: 'bcdChanged'},
10231023
bc: {computed: 'computeBC(b, c)', observer: 'bcChanged'},
10241024
circIn: {type: Number},
10251025
circA: {computed: 'computeCircA(circIn, circB)'},
10261026
circB: {computed: 'computeCircA(circIn, circA)'},
1027+
1028+
x: {type: Number, value: 2},
1029+
y: {type: Number, value: 20},
1030+
z: {type: Number, value: 200},
1031+
xy: {computed: 'computeXY(x, y)', observer: 'xyChanged'},
1032+
computeXY: {computed: 'computeComputeXY(z)'}
10271033
};
10281034
}
10291035
constructor() {
@@ -1034,6 +1040,9 @@ customElements.define('x-computed-ordering', class extends PolymerElement {
10341040
this.abbcdChanged = sinon.spy();
10351041
this.bcdChanged = sinon.spy();
10361042
this.bcChanged = sinon.spy();
1043+
1044+
this.computeXYSpy = sinon.spy();
1045+
this.xyChanged = sinon.spy();
10371046
}
10381047
computeABBCD(a, b, bcd) {
10391048
return a + b + bcd;
@@ -1050,6 +1059,12 @@ customElements.define('x-computed-ordering', class extends PolymerElement {
10501059
computeCircB(circIn, circA) {
10511060
return circIn + (circA || 0);
10521061
}
1062+
computeComputeXY(z) {
1063+
return function computeYZ(x, y) {
1064+
this.computeXYSpy(x, y);
1065+
return x + y + z;
1066+
};
1067+
}
10531068
});
10541069

10551070
class SVGElement extends PolymerElement {

0 commit comments

Comments
 (0)