Skip to content
This repository has been archived by the owner on Mar 23, 2021. It is now read-only.

深入理解Angular的controllerAs语法 #8

Open
huguangju opened this issue Nov 19, 2015 · 0 comments
Open

深入理解Angular的controllerAs语法 #8

huguangju opened this issue Nov 19, 2015 · 0 comments
Labels

Comments

@huguangju
Copy link
Owner

自1.2以来,Angular开发上有些细微变化,其中一个变化我相信有助于改善结构,使用作用域更加合理,并使controller变得更小。

Controller就像大家所了解的class-like对象,驱动模型和视图变更,但它们似乎是围绕着神秘的$scope对象来运作的。Angular的Controller已经改变了$scope的声明方式,因为很多开发人员建议用 this 关键字来替代$scope。

v1.2.0之前的Controller看起来像这样:

// <div ng-controller="MainCtrl"></div>
app.controller('MainCtrl', function ($scope) {
  $scope.title = 'Some title';
});

Controller在这里和$scope是相互独立的, 必须依赖注入它。这样做可能会更好一些:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

完全没有理解也没关系,以后就可以看到这样写的好处。

Controllers as 语法

如果在JavaScript中实例化一个 "class", 你也许会这么做:

var myClass = function () {
  this.title = 'Class title';
}
var myInstance = new myClass();

我们随后就可以用 myInstance 实例访问 myClass函数和它的属性。在Angular中可以用新的 Controller as 语法来以类似的方式实例化。它的声明和绑定如下:

// we declare as usual, just using the `this` Object instead of `$scope`
app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

This is more of a class based setup, and when instantiating a Controller in the DOM we get to instantiate against a variable:
这更多的是基于类的设置,并且当实例化的Controller在DOM中可以获取对应的实例变量:

<div ng-controller="MainCtrl as main">
  // MainCtrl doesn't exist, we get the `main` instance only
</div>

要在DOM中显示 this.title, 需要通过实例名来引用:

<div ng-controller="MainCtrl as main">
   {{ main.title }}
</div>

我认为给scope设置命名空间是一个很大的进步,它可以让Angular不变得那么臃肿。我不喜欢这种 "不受约束的变量"(floating variables), 类似 {{ title }}, 我更喜欢在实例上调用, 像 {{ main.title }}

嵌套作用域(Nested scopes)

Controller as 语法最大的作用就是避免使用嵌套作用域而导致作用域属性的混乱。我们经常会需要在当前作用域内引用父作用域中的属性。

类似下边这样:

<div ng-controller="MainCtrl">
  {{ title }}
  <div ng-controller="AnotherCtrl">
    {{ title }}
    <div ng-controller="YetAnotherCtrl">
      {{ title }}
    </div>
  </div>
</div>

译者注: 嵌套的作用域之间的关系类似JavsScript的原型继承, 内部的作用可以访问外部作用域的属性

三个 {{ title }} 很容易给人造成困扰,我们不清楚它们最终的值到底是什么。如果给它们清楚地指定属于哪个Controller的实例,则可以轻松地跨作用载访问这些属性:

<div ng-controller="MainCtrl as main">
  {{ main.title }}
  <div ng-controller="AnotherCtrl as another">
    {{ another.title }}
    <div ng-controller="YetAnotherCtrl as yet">
      {{ yet.title }}
    </div>
  </div>
</div>

也同样可以不像下边这样访问父作用域:

<div ng-controller="MainCtrl">
  {{ title }}
  <div ng-controller="AnotherCtrl">
    Scope title: {{ title }}
    Parent title: {{ $parent.title }}
    <div ng-controller="YetAnotherCtrl">
      {{ title }}
      Parent title: {{ $parent.title }}
      Parent parent title: {{ $parent.$parent.title }}
    </div>
  </div>
</div>

像这样条理清楚地写:

<div ng-controller="MainCtrl as main">
  {{ main.title }}
  <div ng-controller="AnotherCtrl as another">
    Scope title: {{ another.title }}
    Parent title: {{ main.title }}
    <div ng-controller="YetAnotherCtrl as yet">
      Scope title: {{ yet.title }}
      Parent title: {{ another.title }}
      Parent parent title: {{ main.title }}
    </div>
  </div>
</div>

不会再像那样没完没了地引用$parent。如果Controller的位置在DOM结构中发生改变,$parent.$parent.$parent.$parent 这种引用序列有可能改变! 通过Controller实例别名访问作用域属性会更加合理。

$watchers/$scope 方法

The first time I used the Controller as syntax I was like “yeah, awesome!”, but then to use scope watchers or methods (such as $watch, $broadcast, $on etc.) we need to dependency inject $scope. Gargh, this is what we tried so hard to get away from. But then I realised this was awesome.
Controller as的语法用起来是很爽,但是用作用域监听器或方法(像$watch, $broadcast, $on等等)时我们不是需要依赖注入$scope才行吗! 这个貌似是避免不了的。

The way the Controller as syntax works, is by binding the Controller to the current $scope rather than it being all one $scope-like class-like Object. 对我来说,关键把类和特殊Angular我分离开来。

这样就可以有一个完美的class-like控制器:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

When I need something above and beyond generic bindings, I introduce the magnificent $scope dependency to do something special, rather than ordinary.

Those special things include all the $scope methods, let’s look at an example:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  $scope.$on('someEventFiredFromElsewhere', function (event, data) {
    // do something!
  });
});

解决问题

下边提的 $scope.$watch 的示例,很简单,但很有意思的是,它没有像预期中那样生效:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // doesn't work!
  $scope.$watch('title', function (newVal, oldVal) {});
  // doesn't work!
  $scope.$watch('this.title', function (newVal, oldVal) {});
});

那该怎么办呢? 实际上你可以向$watch的第一个参数传递一个函数:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // hmmm, a function
  $scope.$watch(function () {}, function (newVal, oldVal) {});
});

也就意味着我们可以返回 this.title 的引用:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // nearly there...
  $scope.$watch(function () {
    return this.title; // `this` isn't the `this` above!!
  }, function (newVal, oldVal) {});
});

再用 angular.bind() 改变执行上下文:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // boom
  $scope.$watch(angular.bind(this, function () {
    return this.title; // `this` IS the `this` above!!
  }), function (newVal, oldVal) {
    // now we will pickup changes to newVal and oldVal
  });
});

在$routeProvider/Directives/elsewhere中声明

Controller可以被动态设置,我们不需要总是通过属性绑定它们。在指令内部,像这样写 ControllerAs: property, 很方便地设置:

app.directive('myDirective', function () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    template: [].join(''),
    controllerAs: '', // woohoo, nice and easy!
    controller: function () {}, // we'll instantiate this controller "as" the above name
    link: function () {}
  };
});

$routeProvider内部也一样:

app.config(function ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    controllerAs: '',
    controller: ''
  })
  .otherwise({
    redirectTo: '/'
  });
});

译者注: 其实也可以这样写 controller: 'HomeCtrl as home'

在测试中使用 controllerAs 语法

测试中的 controllerAs 变化不大, 并且我们不再需要注入$scope了。意味着我们测试Controller时也不需要有一个属性的引用(类似 vm.prop), 我们可以简单地使用设置给$controller的变量名。

// controller
angular
  .module('myModule')
  .controller('MainCtrl', MainCtrl);

function MainCtrl() {
  this.title = 'Some title';
};

// tests
describe('MainCtrl', function() {
  var MainController;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller) {
      MainController = $controller('MainCtrl');
    });

  });

  it('should expose title', function() {
    expect(MainController.title).equal('Some title');
  });
});

你也可以在$controller中使用 controllerAs 语法, 不过你需要注入$scope实例到对象中并传入$controller。Controller的别名(scope.main的实例)将会添加到$scope中(像它在实际Angular应用中一样),然而这不是一个优雅的解决方案。

// Same test becomes
describe('MainCtrl', function() {
  var scope;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller, $rootScope) {
      scope = $rootScope.$new();
      var localInjections = {
        $scope: scope,
      };
      $controller('MainCtrl as main', localInjections);
    });
  });

  it('should expose title', function() {
    expect(scope.main.title).equal('Some title');
  });
});

原文: Digging into Angular’s “Controller as” syntax

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

1 participant