emberjs/ember.js — pull request #4747 by cibernox (2014-04-20 11:40:46+00:00)
Title: [FEATURE] Add `invalidate` hook to handle reduceComputed invalidations

Still a WIP.

See #4724 for motivation.

Right now, when any non-array dependent property of `reduceComputed` CPs (and therefore `arrayComputed` CPs too) changes, the whole CP is reset, and `addedItem` is called once per each element in dependent arrays.
Depending on the goal, this behavior is pretty inefficient, specially if there is other CPs depending on this one, because they will also receive tons of changes in waterfall.

This PR add a new `invalidate` hook the the options hash of `reduceComputed`/`arrayComputed` properties where you can specify a more custom behavior.

Very simple example:
```js
var people = [
{name: 'Cris', age: 24},
{name: 'Jane', age: 17},
{name: 'Paul', age: 18},
{name: 'Robb', age: 23},
{name: 'Mark', age: 19},
{name: 'Anne', age: 21},
{name: 'Mike', age: 15},
{name: 'John', age: 16},
{name: 'Kate', age: 20},
{name: 'Bert', age: 22},
];

Ember.Object.extend({
minAgeToDrink: 21,
numberOfBarClients: Ember.reduceComputed('people', 'minDrinkingAge', {
initialValue: 0,
initialize: function(initialValue, changeMeta, instanceMeta){
instanceMeta.drinkingPeople = [];
instanceMeta.notDinkingPeople = [];
instanceMeta.minDrinkingAge = this.get('minDrinkingAge');
},

// Keep updated the array of people who can/cannot drink and the accum.
addedItem: function(accum, person, changeMeta, instanceMeta){
if (person.get('age') >= instanceMeta.minDrinkingAge) {
++accum;
instanceMeta.drinkingPeople.push(person);
} else {
instanceMeta.notDrinkingPeople.push(person);
}
return accum;
},

// Keep updated the array of people who can/cannot drink and the accum.
removedItem: function(accum, person, changeMeta, instanceMeta){
if (person.get('age') < instanceMeta.minDrinkingAge) {
--accum;
instanceMeta.notDrinkingPeople.removeObject(person);
} else {
instanceMeta.drinkingPeople.removeObject(person);
}
return accum;
},

// When the minimum drinking age changes, it can become higher or lower.
// In both cases, only one half of the elements in the `people` array need
// to be checked.
invalidate: function(accum, changeMeta, instanceMeta){
var newMinDrinkingAge = this.get('minDrinkingAge');

if (newMinDrinkingAge > instanceMeta.minDrinkingAge) {
// The min age to drink is higher now. Some people used to drink but now they can't.
// I only need to check among the people that was allowed to drink to check who can't now.
instanceMeta.drinkingPeople = instanceMeta.drinkingPeople.filter(function(person){
if (person.get('age') >= newMinDrinkingAge){
return true
} else {
instanceMeta.notDrinkingPeople.push(person);
return false;
}
});
} else {
// The min age to drink is lower now. Some people was not allowed to drink but now they are.
// I only need to check among the people that was not allowed to drink to check who now can.
instanceMeta.notDrinkingPeople = instanceMeta.notDrinkingPeople.filter(function(person){
if (person.get('age') < newMinDrinkingAge){
return true
} else {
instanceMeta.drinkingPeople.push(person);
return false;
}
});
}
instanceMeta.minDrinkingAge = newMinDrinkingAge;
accum = instanceMeta.drinkingPeople.length;
}
});
}).create({people: people});
```

If not `invalidate` hook is provided, all works as it used to: The full CP is reset and recomputed again.

TODOs:

- [ ] Update documentation
- [ ] Hide behind a feature flag?
- [ ] Should I also pass the previous value of the `changedProperty` to the invalidate hook?
- [ ] Should the `invalidate` hook be executed synchronously of with `Ember.run.once`?