angularjsdependency-injectionangularjs-injector

AngularJS two different $injectors


Today I found, that $injector injected to config or provider is different from $injector injected to service, factory or controller.

And get() function from this $injectors works differently.

$injector from config or provider, can't get() any service! $injector.get('myService') throws Error: [$injector:unpr] Unknown provider: myService, but $injector.has('myService') return true. That's very very strange.

$injector from service or controller works normally.

Here is a code sample for better understanding:

angular.module('app', [])

        .provider('myProvider', function ($injector) {
            this.$get = ['$injector', function (serviceInjector) {
                return {
                    providerInjector: $injector,
                    serviceInjector: serviceInjector
                };
            }];
        })

        .service('myService', function () {})

        .controller('myCtrl', function ($scope, myProvider) {
            var providerInjector = myProvider.providerInjector;
            var serviceInjector = myProvider.serviceInjector;

            console.log(providerInjector === serviceInjector); // -> false

            console.log(serviceInjector.has('myService')); // `serviceInjector` has `myService`
            console.log(getMyService(serviceInjector)); // `serviceInjector` can get `myService`

            console.log(providerInjector.has('myService')); // `providerInjector` has `myService` too!
            console.log(getMyService(providerInjector)); // but `providerInjector` can't get `myService`! =(

            function getMyService(injector) {
                try {
                    injector.get('myService');
                    return "OK";
                } catch (e) {
                    return e.toString();
                }
            }

        });

Here is a plunker to play

Can anybody explain why there is two different injectors?

And how can I use $injector from provider/config to inject service(after service was initialized, of course)?

P.S. I use angular 1.3.13


Solution

  • I found this issue on github: https://github.com/angular/angular.js/issues/5559

    In the config function, $injector is the provider injector, where in the run function, $injector is the instance injector.

    One's the $injector at the config stage (only providers and constants accessible), and one's the $injector at the run stage. The confusion may be that you're thinking the $injector modifies itself to include the new stuff as it crosses the line from config to run, but that's not true. They're two separate (although related) objects, with their own caches of instances.

    A more in-depth reason for this dichotomy will probably come from a deep learning of the $injector internals, but it seems like it's been DRY-ed pretty hardcore, and the two types of injectors share almost all the same behavior, except in how they deal with "cache misses" in their instance caches.

    We are going to overhaul the injector in v2, so this will get fixed there (getting rid of the config phase is one of the objectives of the injector v2).

    Seems like there is really two different injectors, and angular developers will not fix that behavior(in versions <2.0). And nobody added a note about that aspect to $injector docs for some reason.

    I was unable to find a way how to really get instance injector inside a configuration block without hacky tricks. So, I write a cute provider to solve that kind of problems.

    .provider('instanceInjector', function () {
    
        var instanceInjector;
    
        function get() {
            return instanceInjector;
        }
    
        function exists() {
            return !!instanceInjector;
        }
    
        angular.extend(this, {
            get: get,
            exists: exists
        });
    
        this.$get = function ($injector) {
            instanceInjector = $injector;
    
            return {
                get: get,
                exists: exists
            };
        }
    })
    
    // We need to inject service somewhere.
    // Otherwise $get function will be never executed
    .run(['instanceInjector', function(instanceInjector){}])