Thanks to visit codestin.com
Credit goes to github.com

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

深入理解Angular的controllerAs语法 #8

@huguangju

Description

@huguangju

自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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions