のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

AngularJSのfactoryとserviceを読み解く(前編)

AngularJSfactoryserviceがどうにも覚えられないのでまとめてみた。

まとめ

  • factoryはobjectをキャッシュしておく
  • serviceはインスタンス化してキャッシュしておく
  • providerの謎が深まった

factory

providerにfunctionらしきものを渡している。 $getはInjectorに指定されたときに一度だけ呼ばれる。

function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }

https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L666

ここまでだとまだ実行されない。

var app = angular.module('app', ['ngResource']).factory('User', function ($resource) {
  return $resource('/api/user/:id', {id: "@id"}, {});
});

ここで実行され、以降の呼び出しはreturn $resource('/api/user/:id', {id: "@id"}, {});の評価結果がキャッシュされたヤツが呼び出される。

app.controller("UserCtrl", function (User) {
  $scope.user = User.$get(id: 1);
});

service

serviceに指定された無名関数はconstructorという名で$injector.instantiateの引数に渡され、その処理をfactoryへと渡している。

  function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
  }

  function value(name, val) { return factory(name, valueFn(val)); }

  function constant(name, value) {
    assertNotHasOwnProperty(name, 'constant');
    providerCache[name] = value;
    instanceCache[name] = value;
  }

https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L668-L680

$injector.instantiateは与えられた引数を、あたらにConstructorクラスが作られ、serviceに登録した関数のprototypeを継承させいる。 なぜArrayの場合は末尾を取り出しているのかはよくわかってない。 ['$rootScope', function () { ... }]的なアレの為に、Arrayの判定してる。 Constructorクラスをインスタンス化し、invoke(省略)に渡す。

その返り値がObjectまたはFunctionであればそのままをfactoryにわたすが、どちらでもない場合はConstructorインスタンスをfactoryに渡している。

    function instantiate(Type, locals, serviceName) {
      var Constructor = function() {},
          instance, returnedValue;

      // Check if Type is annotated and use just the given function at n-1 as parameter
      // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
      Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype;
      instance = new Constructor();
      returnedValue = invoke(Type, instance, locals, serviceName);

      return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
    }

https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L804-L815

なお、invokeでは(重要なところだけを挙げれば)Constructorクラスのインスタンスをコンテキストとしてserviceで指定した関数を実行しその結果を返しているだけである。

    function invoke(fn, self, locals, serviceName){
      if (typeof locals === 'string') {
        serviceName = locals;
        locals = null;
      }

      var args = [],
          $inject = annotate(fn, strictDi, serviceName),
          length, i,
          key;

      for(i = 0, length = $inject.length; i < length; i++) {
        key = $inject[i];
        if (typeof key !== 'string') {
          throw $injectorMinErr('itkn',
                  'Incorrect injection token! Expected service name as string, got {0}', key);
        }
        args.push(
          locals && locals.hasOwnProperty(key)
          ? locals[key]
          : getService(key)
        );
      }
      if (!fn.$inject) {
        // this means that we must be an array.
        fn = fn[length];
      }

      // http://jsperf.com/angularjs-invoke-apply-vs-switch
      // #5388
      return fn.apply(self, args);
    }

https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L801

ここまで追ってみたけどよくわからないので、続きは明日。

Constructorで空のクラスをprototypeだけ継承し、インスタンス化する。そののちにinvokによってDependenciesを引数としてserviceで指定したfunctionに渡すことで、DIとしての動きを作っているっぽい。

どう使うのかはまだ曖昧だけど、輪郭が見えてきた。

後編: AngularJSのfactoryとserviceを読み解く(後編)