3.3 数据作用域与控制器
了解了模块、组件的概念以及定义创建它们的方法,就可以开始进入各种具体的组件类型的学习了。在图3.1 AngularJS的组件中,可以看到数据作用域Scope(以下简称为作用域)代表了数据模型。而之所以作用域放到控制器Controller(以下简称为控制器)内,是用于说明控制器的主要任务之一是在作用域上定义业务逻辑。具体来说,使用控制器可以对作用域对象进行以下初始化:
● 初始化定义JavaScript属性。
● 初始化添加JavaScript方法。
正因为两者之间的密切联系,本节将这两种AngularJS组件放在一起讨论。
3.3.1 在控制器内初始化作用域对象
可以近似地认为每个控制器都有一个对应的作用域对象,而这个对应的作用域对象是通过$scope这个服务组件来访问的,访问方式与示例3-1的$rootScope类似。
【示例3-2】JavaScript文件片段,在名为myApp的模块内动态定义名为myCtrl的控制器组件,该组件将使用$scope初始化与其对应的作用域对象。
//在myApp模块内动态构建名为myCtrl的控制器组件 //首先获得myApp模块对象 angular.module("myApp"). controller("myCtrl", ["$scope", "$rootScope", function($scope, $rootScope) { $rootScope.rootData={}; //设置根作用域对象上的对象的属性 $rootScope.rootData.appName = "IonicAPP"; //设置根作用域对象上的对象的方法 $rootScope.rootData.sayAppName = function(){ console.log($rootScope.appName); }; $scope.localData = {}; //设置控制器对应的作用域对象上的对象的属性 $scope.localData.dataName = "My Controller Data"; //设置控制器对应的作用域对象上的对象的方法 $scope.localData.sayDataName = function(){ console.log($scope.dataName); }; }]);
【代码解析】此处获取myApp模块实例后直接调用controller方法来定义名为myCtrl的控制器。这次该控制器依赖于AngularJS内置的$rootScope和$scope服务组件。在控制器的内部,$rootScope组件的使用方式与示例3-1没有变化。不同的是使用与调用类似$rootScope的方式来定义了myCtrl控制器对应的数据模型-域对象的localData.dataName属性和localData.sayDataName方法。
3.3.2 使用作用域对象
完成了3.3.1节后,控制器对应的数据模型-域对象已经被初始化好了,那么如何才能在视图中使用它呢?还是用代码来直接描绘典型的代码场景实现片段吧。
【示例3-3】HTML视图片段,在视图里显示和操作myCtrl控制器对应的数据模型-域对象的属性。
<body ng-app="myApp"> <div ng-controller="myCtrl"> {{ rootData.appName }} <! -- 此处将会执行单向绑定,显示"IonicAPP"--> {{ localData.dataName }} <! -- 此处将会执行单向绑定,初始时显示"My Controller Data" --> <label>请输入数据</label> <input ng-model="localData.dataName"> <! -- 此处将会执行双向绑定,初始时显示"My Controller Data",用户输入任意数据后将被自动同步到数据模型--> </div> </body>
【代码解析】此处代码有几个地方与普通的HTML代码不一样。其中分别是:
● body标签的ng-app属性代表了这是一个AngularJS应用的页面,body标签内部的内容展现和事件处理将会转由AngularJS框架进行统一管理。在3.6节读者将能看到将ng-app的属性设置为具体某模块名的意义与带来的结果说明。
● div标签的ng-controller属性代表了该标签与其内部所绑定对应的数据模型就是在myCtrl控制器内通过$scope定义的域对象。因此该域对象拥有localData.dataName属性和localData.sayDataName方法。而AngularJS框架在执行到这里的时候,会创建myCtrl控制器的实例,从而也就创建了一份它对应的数据模型的实例。
● 事实上,使用过PHP、Ruby on Rails、Express等服务器端HTML模板技术的读者对这种使用{{}}来绑定在控制器的成员变量值并显示在视图里的写法应该并不陌生。{{ rootData.appName }}代表了读取myCtrl控制器的域对象或是该域对象的继承树中位于上方的上级域对象的appName属性。此处因为myCtrl控制器的域对象内并没有rootData对象属性,因此最终上溯,找到了使用$rootScope定义的rootData对象属性,随后类似于通过代码读取了$rootScope.rootData.appName的值,并用这个文本值直接替换掉{{ rootData.appName }}。
● {{ localData.dataName }}的处理方法与{{ rootData.appName }}类似,只不过因为myCtrl控制器的域对象内有localData对象属性,因此不用再向上查找,类似于通过代码直接读取$scope.localData.dataName的值,并把它的文本值直接替换掉{{ localData.dataName }}。
● input标签的ng-model属性负责了将控件的value与myCtrl控制器的域对象的localData.dataName对象属性进行双向绑定。最终产生的效果是:页面初始加载后,输入栏的值为“My Controller Data”;而当用户修改输入栏的值时(假设输入了“Hello My Controller”),用户的输入值被同步到绑定的$scope.localData.dataName里,而上面{{ localData.dataName }}由于单向绑定的关系,将会随着用户的输入,不断刷新显示输入栏的值,最后显示的值也是“Hello My Controller”。这里值得一提的ng-model正是一个我们在3.4节要学习的指令(Directive),它构成了图3.1中视图-模型(View-Model)的部分。
综合示例3-2和示例3-3,可以发现作用域对象在基于AngularJS框架开发中占据非常重要的位置,因为它就是我们常说的数据模型。而用来操作作用域对象的$scope服务组件,也因而意义重大。为了帮助读者在后续的开发中少走弯路,笔者这里列举出来一些$scope的特点供揣摩理解:
● $scope是一个POJO(Plain Old JavaScript Object)。
● $scope提供了一些工具方法$watch/$apply。
● $scope是表达式的执行环境(或者叫作用域)。
● $scope是一个树型结构,与HTML页面里DOM的标签平行。
● $scope对象会继承父$scope上的属性和方法。
● 每一个AngularJS应用只有一个根$scope对象(一般属于声明了ng-app属性的容器元素),可使用$rootScope直接访问。
● $scope可以传播事件,类似DOM事件,可以向上(使用emit方法)也可以向下(使用broadcast方法)。
● $scope不仅是建立MVVM的一部分,也是实现双向数据绑定的基础。
提示
这里对$scope的介绍非常简单,更深入地了解是很有必要的。笔者推荐学有余力的读者可以参阅专门介绍AngularJS框架主题的书籍与作用域有关的章节,本书就不再重复着重介绍了。
3.3.3 控制器与作用域的反模式
AngularJS的初学者出于直觉或者省事考虑,很自然地就会往控制器与作用域放置不必要的业务逻辑。然而正确的做法是,控制器这一层应该很薄。也就是说,应用里大部分的业务逻辑和持久化数据都应该放在AngularJS的服务组件里。出于内存性能的考虑,控制器实例只在需要的时候才会初始化,一旦不需要就会被抛弃。因此当用户切换或刷新视图时,AngularJS框架会清空当前的控制器以及与其对应的作用域。而AngularJS的服务组件是一旦创建即常驻内存的单例对象,除了可以用于包装业务逻辑,提供永久保存应用数据的接口外,服务组件也可以在不同的控制器之间通过依赖注入的方式被使用。
关于控制器与作用域,AngularJS有以下几个总结出来的控制器与作用域反模式(即不被推荐的一些不规范做法):
● DOM操作:应当将DOM操作使用指令进行封装,本书3.4节将介绍指令。
● 变换输出形式:应当使用过滤器对输出显示进行转化,本书3.4节将介绍过滤器。
● 控制器内有复杂的业务代码:业务代码应当使用服务进行封装,本书3.5节将介绍服务类组件。