-
Notifications
You must be signed in to change notification settings - Fork 31
深入理解Angular的controllerAs语法 #8
Description
自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');
});
});