to initComponent() or not to initComponent()

2019-01-08 05:32发布

问题:

I struggle when building an app in ExtJS 4, and part of that is confusion on when to configure something in initComponent() and when not to...

For example, in Sencha's own MVC Application Architecture doc, when first creating the grid view, they defined the inline store in the initComponent() method. (See "Defining a View" section)

Further down, when they factored out the store into a separate class, they moved the definition outside of initComponent(). There is a helpful comment that draws attention to this fact, but there is no explanation. (See Creating a Model and Store section)

I guess the reason is supposed to be obvious, but I'm missing it. Any pointers?

回答1:

If you do not have a deep understanding of how ExtJS class system work, you may want to follow this:

Declare all non-primitive types in initComponent().

Terminology

  • Primitive types - strings, booleans, integers, etc.
  • Non-Primitives - arrays & objects.

Explanation

If the component you extend is to be created more than once, any non-primitive configs declared as a config option (outside initComponent) will be shared between all instances.

Because of this, many people experienced issues when an extended component (typically an extended grid) is created on more than one tab.

This behaviour is explained in sra's answer below and in this Skirtle's Den article. You may also want to read this SO question.



回答2:

First I will take a stand on my Comments:

@AlexanderTokarev Don't get me wrong. I don't talk about configurations of components, or much worse of instances and moving them to initComponent(), that is not my point.

Now what I think about this.

initComponent() should resolve anything required the time an instance of this class is created. No more, no less.

You can mess up a load when defining classes and most of it happens because people don't understand how the ExtJS class-system works. As this is about components, the following will focus on those. It will also be a simplified example which should only show a sort of error that I've seen a lot of times.

Let's start: We have a custom panel which does a lot of nifty things. That brings up the need of a custom configuration, we call it foo. We add it along with our default config option to the class definition so we can access it:

Ext.define('Custom', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.custpanel',

    foo: {
        bar: null  
    },

    initComponent: function() {
        this.callParent(arguments);
    }
});

But things get weird after testing. The values of our configurations seems to change magically. Here's a JSFiddle What happened is that all created instances are referring to the same foo instance. But lately I've done that

store: {
    fields: [ ... ],
    proxy: {
        type: 'direct',
        directFn: 'Direct.Store.getData'
    }
}

with a store and that worked. So why doesn't foo work?

Most people don't see any difference between this little foo object and an (ExtJS) config which is basically correct because both are objects (instances). But the difference is that all classes shipped by sencha know perfectly well which configuration properties they expect and take care of them.

For example the store property of a grid is resolved by the StoreManager and can therefore be:

  • storeId string, or
  • store instance or
  • store configuration object.

During the initialization of the grid either of these get resolved and overwritten by an actual store instance. A store is just one example. I guess the more known one is the items array. This is an array at definition time and it gets overridden for each instance with a MixedCollection (if I am not mistaken).

Yes there is a difference between a class definition and the instance created from it. But we need to take care of any new property which contains a reference like the foo from above and that is not that complicated. Here is what we need to do to fix it for the foo example

Ext.define('Custom', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.custpanel',

    foo: {
        bar: null  
    },

    initComponent: function() {
        this.foo = Ext.apply({}, this.foo);
        this.callParent(arguments);
    }
});

Here's the JSFiddle

Now we take care of the foo config when an instance get created. Now this foo example is simplified and it will not always be that easy to resolve a configuration.

Conclusion

Always write your class definition as configurations! They must not contain any referred instances except for plain configuration and must take care of these to resolve them when a instance get created.

Disclaimer

I do not claim to cover all with this really short writing!



回答3:

I usually advocate for having as much configuration as possible in class config options, because it reads better and is easier to override in subclasses. Besides that, there is a strong possibility that in future Sencha Cmd will have optimizing compiler so if you keep your code declarative, it could benefit from optimizations.

Compare:

Ext.define('MyPanel', {
    extend: 'Ext.grid.Panel',

    initComponent: function() {
        this.callParent();
        this.store = new Ext.data.Store({
            fields: [ ... ],
            proxy: {
                type: 'direct',
                directFn: Direct.Store.getData
            }
        });
        this.foo = 'bar';
    }
});

...

var panel = new MyPanel();

And:

Ext.define('MyPanel', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.mypanel',

    foo: 'bar',

    store: {
        fields: [ ... ],
        proxy: {
            type: 'direct',
            directFn: 'Direct.Store.getData'
        }
    }
});

...

var panel = Ext.widget({
    xtype: 'mypanel',
    foo: 'baz'
});

Note how these approaches are very different. In the first example, we're hardcoding a lot: object property values, store configuration, MyPanel class name when it's used; we're practically killing the idea of a class because it becomes inextensible. In the second example, we're creating a template that can be reused many times with possibly different configuration - basically, that's what the whole class system is about.

However, the actual difference lies deeper. In the first case, we're effectively deferring class configuration until runtime, whereas in the second case we're defining class configuration and applying it at very distinctively different phases. In fact, we can easily say that the second approach introduces something JavaScript lacks natively: compile time phase. And it gives us a plethora of possibilities that are exploited in the framework code itself; if you want some examples, take a look at Ext.app.Controller and Ext.app.Application in latest 4.2 beta.

From more practical perspective, the second approach is better because it's easier to read and deal with. Once you grasp the idea, you will find yourself writing all your code like that, because it's just easier this way.

Look at it this way: if you would write an old style Web application, generating HTML and stuff on the server side, you would try not to have any HTML mixed with the code, would you? Templates to the left, code to the right. That's practically the same as hardcoding data in initComponent: sure it works, up to a point. Then it becomes a bowl of spaghetti, hard to maintain and extend. Oh, and testing all that! Yuck.

Now, there are times when you need to do something with an instance at runtime, as opposed to a class definition time - the classical example is applying event listeners, or calling control in Controllers. You will have to take actual function references from the object instance, and you have to do that in initComponent or init. However, we're working on easing this problem - there should be no hard requirement to hardcode all this; Observable.on() already supports string listener names and MVC stuff will too, shortly.

As I said in the comments above, I'll have to write an article or guide for the docs, explaining things. That would probably have to wait until 4.2 is released; meanwhile this answer should shed some light on the matter, hopefully.



回答4:

I've been searching for an answer to the same question when I ended up here and seeing these answers made me disappointed. None of these answer the question: initComponent() or constructor?

It's nice to know that class config option objects are shared and you need to initialize/process them per instance, but the code can go into the constructor as well as the initComponent() function.

My guess was that the constructor of the Component class calls initComponent() somewhere in the middle and I wasn't very wrong: Just had to look at the source code, it's actually AbstractComponent's constructor.

So it looks like this:

AbstractComponent/ctor:
- stuffBeforeIC()
- initComponent()
- stuffAfterIC()

Now if you extend a Component, you'll get something like this:

constructor: function () {
  yourStuffBefore();
  this.callParent(arguments);
  yourStuffAfter();
},
initComponent: function () {
  this.callParent();
  yourInitComp()
}

The final order these get called in is:

- yourStuffBefore()
- base's ctor by callParent:
  - stuffBeforeIC()
  - initComponent:
    - base's initComponent by callParent
    - yourInitComp()
  - stuffAfterIC()
- yourStuffAfter()

So in the end it all depends on whether you want/need to inject your code between stuffBeforeIC and stuffAfterIC which you can look up in the constructor of the class that your are going to extend.