I have inherited a codebase written in Ember 0.9.8.1
In some cases it can be very slow.
I'm in the process of narrowing down why it is slow.
I have noticed the following.
A function in an ArrayController is called to load data.
To load the data it gets json from the backend (fast) and then for every row it creates an (previously defined) Ember.Object (slow) and push that to the content[] of the ArrayController.
Example:
App.ExOb = Ember.Object.extend({
data1: null,
data2: null,
func1: function () { // statements }.property('data1').cacheable()
func2: function () { // statements }.property('data2').cacheable()
..etc..
})
App.lotsOfData = Ember.ArrayController.create({
content: [],
loaddata: function() {
var self=this;
get_data().forEach(function (row, index) {
var d = App.ExOb.create(row.data);
self.pushObject(d);
}
}
})
I'm trying to figure out why the creation and push of the Ember.Object is slow.
What I did notice was that on creation of the object (in the example App.ExOb.create()) every property function of the object (in the example func1() and func2()) is called.
I've tried a small bit of ember code to see why this would happen but can't seem to emulate this. The only time I can see the computed property being executed is when I do a get() of that property.
Can anyone tell me (or point me to documentation that I missed) when a computed property function is executed (other than doing a get() ofcourse :-) )?
Edit:
So far I found the following reasons to execute a computed property:
1. Calling/using the property directly
2. Using the propery in a handlebars template that is shown in the browser.
This is just a quick stab at the problem, but have you tried to create the ExOb and initialize its properties manually, something like:
self.pushObject(App.ExOb.create({
data1: row.data.data1,
data2: row.data.data2,
... etc ...
});
That might be faster, as Ember doesn't have to guess at what to copy from the row.data object and into the App.ExOb object.
Generally, the func1() and func2() functions wouldn't need to be overloaded upon object creation in my mind, but your requirements might dictate otherwise.
Related
Using version 2.17. I have an Ember component inside an /edit route with a controller:
// edit.hbs
{{ingredient-table recipe=model ingredients=model.ingredients}}
Inside my component, I am using a didRecieveAttrs hook to loop through ingredients on render, create proxy objects based off of each, and then build an ingredient table using those proxy objects.
// ingredient-table.js
didReceiveAttrs() {
let uniqueIngredients = {};
this.get('ingredients').forEach((ingredient) => {
// do some stuff
})
this.set('recipeIngredients', Object.values(uniqueIngredients));
}
I also have a delete action, which I invoke when a user wishes to delete a row in the ingredient table. My delete action looks like this:
// ingredient-table.js
deleteIngredient(ingredient) {
ingredient.deleteRecord();
ingredient.save().then(() => {
// yay! deleted!
})
}
Everything mentioned above is working fine. The problem is that the deleted ingredient row remains in the table until the page refreshes. It should disappear immediately after the user deletes it, without page refresh. I need to trigger the didReceiveAttrs hook again. If I manually call that hook, all my problems are solved. But I don't think I should be manually calling it.
Based on the docs, it is my understanding that this hook will fire again on page load, and on re-renders (not initiated internally). I'm having some trouble figuring out what this means, I guess. Here's what I've tried:
1) calling ingredients.reload() in the promise handler of my save in ingredient-table.js (I also tried recipe.reload() here).
2) creating a controller function that calls model.ingredients.reload(), and passing that through to my component, then calling it in the promise handler. (I also tried model.reload() here).
Neither worked. Am I even using the right hook?
I suppose recipeIngredients is the items listed in the table. If that is the case; please remove the code within didReceiveAttrs hook and make recipeIngredients a computed property within the component. Let the code talk:
// ingredient-table.js
recipeIngredients: Ember.computed('ingredients.[]', function() {
let uniqueIngredients = {};
this.get('ingredients').forEach((ingredient) => {
// do some stuff
})
return Object.values(uniqueIngredients)
})
My guess is didReceiveAttrs hook is not triggered again; because the array ingredients passed to the component is not changed; so attrs are not changed. By the way; do your best to rely on Ember's computed properties whenever possible; they are in the hearth of Ember design.
I am trying to assign default values for one of my components at init lifecycle hook; in case an undefined value is passed from parent component. Initially everything seems to be working as expected; however when my component is forced a re-render (possibly via another property value update at parent component); undefined value at parent component is written back to my component.
This means; the value assignment I made during initialization is not reflected to parent component, in other words two-way-binding is temporarily not working (the values of parent and child components are not synchronized). Is it the expected behavior or am I missing sth. important about init event? Where is the appropriate place to initialize undefined values of a component? See the twiddle for a simple illustration.
Okay, first a way to work around is to use the update function on the attr. Checkout this twiddle.
I replaced this.set('name', 'tom') with this.attrs.name.update('tom') in the .js and {{name}} with {{attrs.name}} in the .hbs.
Another way to work around is just to wrap the asignment in a Ember.run.later like I've done here, where I replaced this.set('name', 'tom') with that:
Ember.run.later(() => {
this.set('name', 'tom');
});
So that are the workarounds.
The fact that the bindings are not completely set up on init has a long history. But generally its an anti pattern to initialize a value in a child component and give this value up to the parent. It makes your code less readable and is agains the DDAU (Data down, Actions up) principle.
I recommend to initialize the data explicit when you create your models, not implicit during your component evaluation cycle.
For default values that you don't want to store I recommend you to write a computed property:
nameWithDefault: computed('name', {
get() {
return get(this, 'name') || 'tom';
},
set(key, val) {
set(this, 'name', val);
}
})
This is explicit, does not write down the data after a component is viewed, and works for the user.
I agree with the solution from Lux but if you want to try something else, maybe on your child component you could add a computed property that checks the name or default to another string. This allow you to have different default values on the different child templates (assuming you have different child components that use the same name property).
child-component.js
export default Ember.Component.extend({
child_name: Ember.computed('name', function() {
return this.get('name') || 'tom';
}),
});
child-component.hbs
{{child_name}}
<br>
{{surname}}
To preface this, I must clarify that I am using the legacy-controller and the legacy-view for the interim period while transitioning to Ember 2.3, found here:
https://github.com/emberjs/ember-legacy-controllers
Now, I have a property called currentTopPost on my (legacy) controller.
In Ember 1.7, I had this setup:
// before observer
currentTopPostBeforeObserver: function(){
...
}.observesBefore('currentTopPost'),
// observer
currentTopPostBeforeObserver: function(){
...
}.observes('currentTopPost'),
The reason I had it this way was that when the currentTopPost changed, I wanted it save the old topPost before it switched its value to the new property, as it was a Post object (I had a Post model).
Of course, in 1.7, I saved the old post in the beforeObserver and then did whatever else I had to do in the observer. Now, In Ember 2.3, I have this set up:
currentTopPostObserver: Ember.observer('currentTopPost', function(){
...
}),
Which works fine as far as performing functions with the new value goes. But I've lost the ability to process an action before the value changes. Now according to an answer to this question:
How can an observer find out the before and after values of the observed property in Ember.js?
the observesBefore function has been deprecated and we should be following this:
doSomething: Ember.observer( 'foo', function() {
var foo = this.get('foo');
var oldFoo = this.get('_oldFoo');
if (foo === oldFoo) { return; }
// Do stuff here
this.set('_oldFoo', foo);
})
However, on trying to use this.get("_oldCurrentTopPost"), I get nothing. How do I access the old value of this property before it changes ?
What I use as a replacement is:
propWillChange(prop) {
//your new before observer
},
propDidChange: Ember.observer('prop', function() {
let prop = this.get('prop');
if (this._oldProp !== prop) {
this.propWillChange(this._oldProp);
this._oldProp = prop;
}
//Do stuff
})
Of course, on the first run, _oldProp will be undefined, but that's expected, right? It is the first time prop is being changed.
I also disagree that observers shouldn't be used. I agree that observers should be avoided if possible, because many people don't fully grasp their side-effects, but in many cases they are very useful, especially when building 3rd party plugin integrations.
Since the question specifically asks about a beforeObserver replacement, here it is. However, I recommend to reconsider if your use case can be rebuilt without observers if possible.
Twiddle: https://ember-twiddle.com/045b7b9c1562ceb6bbdc
As far as I can tell, there is no particularly nice way to get that mechanism back. Observers themselves are "considered harmful" in many cases, but I'll try my best to give you a practical alternative solution.
The best quick and relatively dirty way to do this that I can think of is to make a "proxy" computed property with a getter and setter. Within the setter you can get the previous value of the "real" property, call out to a function to do whatever, and then set the new value on the real property.
Here's an example that you could use:
myProxyProperty: Ember.computed('myRealProperty', {
get() {
return this.get('myRealProperty');
},
set(key, value) {
const oldValue = this.get('myRealProperty');
this.doSomethingWithOldValue(oldValue);
this.set('myRealProperty', oldValue);
}
})
Unfortunately I don't know of a better way of doing this in new Ember at the moment.
I'm trying to update a component value dynamically from the controller, without explicitly passing the value through the template. The following hard-coded solution works:
foo: Em.computed.oneWay('bar')
however, I'm looking to have something like this:
slab: 'bar',
foo: Em.computed.oneWay(slab)
Its important that the values are bound together, and not just the initial value when the component starts up. Is there a way to do this?
Update January 2015: Coincidentally, I wrote a small addon for this a few days ago in response to an issue in the Ember Github. This addon allows you to have dynamic computed properties. So this would now be your solution:
slab: 'bar',
foo: Ember.computed.indirect('slab')
That will keep the two bound together, so the foo property will always point at whichever property the slab value refers to.
Maybe something like this (taking from your example):
slab: function() {
return mysteryString;
}.property(),
foo: undefined,
_slabObserver: null,
_slabObserverKey: null,
_slabDidChange: function() {
// Remove the old observer
if (this.get('_slabObserverKey')) {
this.removeObserver(this, this.get('_slabObserverKey'), this.get('_slabObserver'));
}
// Create the new one
this.set('_slabObserver', function() {
// Update foo when the value whose key is held by 'slab' changes
this.set('foo', this.get(this.get('slab')));
});
// Watch the property whose key is held by 'slab' and call the method we just created
this.addObserver(this, this.get('slab'), this.get('_slabObserver'));
// Hold the last key so we can remove the observer properly after it changes
this.set('_slabObserverKey', this.get('slab'));
}.property('slab')
To walk through it:
The controller is created and slab is undefined. Something causes slab to change, triggering the _slabDidChange observer. (Note that we use property and not observes on _slabDidChange because the former would cause the function to get called too soon. Source)
_slabDidChange fires. It skips removing the old observer because we haven't created one yet. It then adds a new observer that watches the property held by the slab property. It then stores the key and observer so we can remove them later.
foo is updated whenever the property we observed changes. In your example, anytime bar updates, foo updates.
If slab changes, step 2 repeats, only it removes the old observer first, as to not have conflicting updates to foo.
The idea is that you have to add the observer to the bar property manually. I haven't tested this, but the idea should be clear.
try this:
slab: 'bar',
foo: Em.computed.oneWay(this.slab);
As far as I know, you can only do this with a computed property:
foo: function(key, value) {
var slab = this.get('slab');
if (arguments.length > 1) {
this.set(slab, value);
}
return this.get(slab);
}.property('slab')
For oneWay, you will have to change this.set(slab, value); or just remove the whole if.
Note the below Ember view definition. If I remove the didInsertElement call or comment out the get('controller') call, the setupMultiselect observer never gets called. Is this a feature or a bug? Confused...
Discourse.KbRelatedObjView = Discourse.View.extend({
...
didInsertElement: function() { var self = this;
// for some reason this needs to be here else the observer below never fires
self.get('controller');
},
setupMultiselect: function() { var self = this;
...
}.observes('controller.objPage')
});
I wouldn't say it's a feature or a bug, more like a quirk. It is the expected behavior though. It's noted here.
UNCONSUMED COMPUTED PROPERTIES DO NOT TRIGGER OBSERVERS
If you never get a computed property, its observers will not fire even if its dependent keys change. You can think of the value changing from one unknown value to another.
This doesn't usually affect application code because computed properties are almost always observed at the same time as they are fetched. For example, you get the value of a computed property, put it in DOM (or draw it with D3), and then observe it so you can update the DOM once the property changes.
If you need to observe a computed property but aren't currently retrieving it, just get it in your init method.