在Rails等传统Web框架中,控制器将多个模型中的数据和模板组合在一起形成视图,并将其提供给用户,这个组合过程会产生一个单向视图。
AngularJS则采用了完全不同的解决方案。它创建实时模板来代替视图,而不是将数据合并进模板之后更新DOM。任何一个独立视图组件中的值都是动态替换的。这个功能可以说是AngularJS中最重要的功能之一
ng-app属性声明所有被其包含的内容都属于这个AngularJS应用,只有被具有ng-app属性的DOM元素包含的元素才会受AngularJS影响。
AngularJS会记录数据模型所包含的数据在任何特定时间点的值,
当AngularJS认为某个值可能发生变化时,它会运行自己的事件循环来检查这个值是否变“脏”。如果该值从上次事件循环运行之后发生了变化,则该值被认为是“脏”值。这也是Angular可以跟踪和响应应用变化的方式
这个事件循环会调用$digest()循环,这个过程被称作脏检查(dirty checking) 。脏检查是检查数据模型变化的有效手段, AngularJS会在事件循环时执行脏检查(第24章会深入讨论)来保证数据的一致性。
为了表示内部和内置的库函数,Angular使用$预定义对象。尽管这类似于全局的jQuery对象$,但它们是完全无关的。只要遇到$符号,你都可以只把它看作一个Angular对象。
简单的数据绑定
使用ng-model指令将内部数据模型对象($scope)中的name属性绑定到了文本输入字段上,无论在文本输入字段中输入了什么,都会同步到数据模型中。$scope对象是一个简单的JavaScript对象,其中的属性可以被视图访问,也可以同控制器进行交互。
<input type="text" ng-model = "name" ><br> <div>hello {{name}}</div>
DOM 元 素 上 的ng-controller声明所有被它包含的元素都属于某个控制器。
一个显示时间的小例子:
<script type="text/javascript" src="angular.js"></script> <script type="text/javascript" src="app.js"></script> <div ng-app="testmodule"> <div ng-controller="MyController"> <span>{{clock}}</span> </div> </div>
app.js 中的内容:
angular.module('testmodule',[]) .controller("MyController",['$scope',function($scope){ $scope.clock = new Date(); var up = function(){ $scope.clock = new Date(); } setInterval(function(){ $scope.$apply(up); },1000); }]);
作用域(scope)
作用域是视图和控制器之间的胶水。视图中的模板会和作用域进行连接,应用会对DOM进行设置以便将属性变化通知给AngularJS。
作用域是应用状态的基础。基于动态绑定,可以依赖视图在修改数据时立刻更新$scope,也可以依赖$scope在其发生变化时立刻重新渲染视图。
AngularJS将$scope设计成和DOM类似的结构,因此$scope可以进行嵌套,也就是说我们可以引用父级$scope中的属性。
作用域提供了监视数据模型变化的能力。它允许开发者使用其中的apply机制,将数据模型的变化在整个应用范围内进行通知。
AngularJS启动并生成视图时,会将根ng-app元素同$rootScope进行绑定。 $rootScope是所有$scope对象的最上层。
$rootScope是AngularJS中最接近全局作用域的对象,可以看作是 根作用域,在$rootScope上附加太多业务逻并不是好主意,这与污染JavaScript的全局作用域是一样的。
将应用的业务逻辑都放在控制器中,而将相关的数据都放在控制器的作用域中,这是非常完美的架构。
$scope对象就是一个普通的JavaScript对象,我们可以在其上随意修改或添加属性。
$scope对象在AngularJS中充当数据模型,$scope并不负责处理和操作数据,
作用域能做什么
有以下的基本功能:
提供观察者以监视数据模型的变化;
可以将数据模型的变化通知给整个应用,甚至是系统外的组件;
可以进行嵌套,隔离业务功能和数据;
给表达式提供运算时所需的执行环境。
$scope 的生命周期
当Angular关心的事件发生在浏览器中时,比如用户在通过ng-model属性监控的输入字段中输入,或者带有ng-click属性的按钮被点击时, Angular的事件循环都会启动
每当事件被处理时, $scope就会对定义的表达式求值。此时事件循环会启动,并且Angular应用会监控应用程序内的所有对象,脏值检测循环也会运行。
$scope对象的生命周期处理有四个不同阶段。
.1 创建
创建控制器或指令时, AngularJS会用$injector创建一个新的作用域,并在这个新建的控制器或指令运行时将作用域传递进去。
.2 链接
当Angular开始运行时,所有的$scope对象都会附加或者链接到视图中。所有创建$scope对象的函数也会将自身附加到视图中。这些作用域将会注册当Angular应用上下文中发生变化时需要运行的函数。
这些函数被称为$watch函数, Angular通过这些函数获知何时启动事件循环。
.3 更新
当事件循环运行时,它通常执行在顶层$scope对象上(被称作$rootScope),每个子作用域都执行自己的脏值检测。每个监控函数都会检查变化。如果检测到任意变化, $scope对象就会触
发指定的回调函数。
.4 销毁
当一个$scope在视图中不再需要时,这个作用域将会清理和销毁自己。
尽管永远不会需要清理作用域(因为Angular会为你处理),但是知道是谁创建了这个作用域还是有用的,因为你可以使用这个$scope上叫做$destory()的方法来清理这个作用域。
表达式
{{ }}符号将一个变量绑定到$scope上的写法本质上就是一个表达式: {{ expression }}。 当用$watch进行监听时, AngularJS会对表达式或函数进行运算。
表达式和eval(javascript)非常相似,但是由于表达式由AngularJS来处理,它们有以下显著不同的特性:
所有的表达式都在其所属的作用域内部执行,并有访问本地$scope的权限;
如果表达式发生了TypeError和ReferenceError并不会抛出异常;
不允许使用任何流程控制功能(条件控制,例如if/eles);
可以接受过滤器和过滤器链。
AngularJS通过$parse【解析】这个内部服务来进行表达式的运算,这个服务能够访问当前所处的作用域。这个过程允许我们访问定义在$scope上的原始JavaScript数据和函数。
将$parse服务注入到控制器中,然后调用它就可以实现手动解析表达式。举例来说,如果页面上有一个输入框绑定到了expr变量上,
这是一个数据双向绑定的例子,目前还不知道$parse的用法,
<div ng-controller="ParentController"> <input type="text" ng-model="expr" placeholder="请输入"><br> <div>{{parseValue}}</div> </div> var app = angular.module("app",[]); app.controller("ParentController",['$scope','$parse',function($scope,$parse){ $scope.$watch('expr',function(newVal,oddVal,scope){ if(newVal !== oddVal){ // 让我们建立parseFun表达式,目前 $prase()怎么解析还不知道
}
});
}]);
插值字符串
在AngularJS中,我们的确有手动运行模板编译的能力,例如,在字符串模板中做插值操作,需要在你的对象中注入$interpolate服务。
$interpolate服务是一个可以接受三个参数的函数,其中第一个参数是必需的。
text(字符串):一个包含字符插值标记的字符串。
mustHaveExpression(布尔型):如果将这个参数设为true,当传入的字符串中不含有表达式时会返回null。
trustedContext(字符串): AngularJS会对已经进行过字符插值操作的字符串通过$sec.getTrusted()方法进行严格的上下文转义。
<div class="panel panel-primary" ng-app="app"> <div class="panel-heading"> <div class="panel-title">双向数据绑定</div> </div> <div class="panel-body" ng-controller="ParentController"> <input type="email" ng-model="name" placeholder="请输入"><br> <textarea ng-model="emailbody" ></textarea> <pre>{{parseValue}}</pre> </div> </div> app.js: var app = angular.module("app",[]); app.controller("ParentController",function($scope,$interpolate){ $scope.$watch('emailbody',function(body){ if(body){ var tem = $interpolate(body); $scope.parseValue = tem({name : $scope.name}); } }); });
效果:
AngularJS 的生命周期
总共有两个主要阶段: 编译阶段 和 链接阶段
编译阶段
第一个阶段是编译阶段。在编译阶段, AngularJS会遍历整个HTML文档并根据JavaScript中的指令定义来处理页面上声明的指令。
每一个指令的模板中都可能含有另外一个指令,另外一个指令也可能会有自己的模板。当AngularJS调用HTML文档根部的指令时,会遍历其中所有的模板,模板中也可能包含带有模板的指令。
模板树可能又大又深,尽管元素可以被多个指令所支持或修饰,这些指令本身的模板中也可以包含其他指令,但只有属于最高优先级指令的模板会被解析并添加到模板树中。
将包含模板的指令和添加行为的指令分离开来。
一旦对指令和其中的子模板进行遍历或编译,编译后的模板会返回一个叫做模板函数的函数。我们有机会在指令的模板函数被返回前,对编译后的DOM树进行修改。
以ng-repeat为例,它会遍历指定的数组或对象,在数据绑定之前构建出对应的DOM结构。 只会有很少的性能开销。
一个指令的表现一旦编译完成,马上就可以通过编译函数对其进行访问,编译函数的签名包含有访问指令声明所在的元素(tElemente)及该元素其他属性(tAttrs)的方法。这个编译函数返回前面提到的模板函数,其中含有完整的解析树。
编译函数:conpile()
compile函数 可以返回一个对象或函数。理解compile和link选项是AngularJS中需要深入讨论的高级话题之一,对于了解AngularJS究竟是如何工作的至关重要。
compile选项本身并不会被频繁使用,但是link函数则会被经常使用。本质上,当我们设置了link选项,实际上是创建了一个postLink()链接函数,以便compile()函数可以定义链接函数。
如果设置了compile函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的
compile和link选项是互斥的。如果同时设置了这两个选项,那么会把compile所返回的函数当作链接函数,而link选项本身则会被忽略。
compile: function(tEle, tAttrs, transcludeFn) { var tplEl = angular.element('<div>' +'<h2></h2>' +'</div>'); var h2 = tplEl.find('h2'); h2.attr('type', tAttrs.type); h2.attr('ng-model', tAttrs.ngModel); h2.val("hello"); tEle.replaceWith(tplEl); return function(scope, ele, attrs) { // 连接函数 }; }
不要进行DOM事件监听器的注册:这个操作应该在链接函数(link)中完成。
编译函数负责对模板DOM进行转换。 链接函数负责将作用域和DOM进行链接。
link函数
用link函数创建可以操作DOM的指令。
链接函数是可选的。如果定义了编译函数,它会返回链接函数,因此当两个函数都定义了时,编译函数会重载链接函数。
如果我们的指令很简单,并且不需要额外的设置,可以从工厂函数 (回调函数)返回一个函数来代替对象。如果这样做了,这个函数就是链接函数
下面两种定义指令的方式在功能上是完全一样的:
angular.module('myApp', []) .directive('myDirective', function() { return { pre: function(tElement, tAttrs, transclude) { // 在子元素被链接之前执行 // 在这里进行Don转换不安全 // 之后调用'lihk'h函数将无法定位要链接的元素 }, post: function(scope, iElement, iAttrs, controller) { // 在子元素被链接之后执行 // 如果在这里省略掉编译选项 //在这里执行DOM转换和链接函数一样安全吗 } }; }); angular.module('myApp', []) .directive('myDirective', function() { return { link: function(scope, ele, attrs) { return { pre: function(tElement, tAttrs, transclude) { // 在子元素被链接之前执行 // 在这里进行Don转换不安全 // 之后调用'lihk'h函数将无法定位要链接的元素 }, post: function(scope, iElement, iAttrs, controller) { // 在子元素被链接之后执行 // 如果在这里省略掉编译选项 //在这里执行DOM转换和链接函数一样安全吗 } } } } });
当定义了编译函数来取代链接函数时,链接函数是我们能提供给返回的 就是 postLink函数。
链接函数的作用。它会在模板编译并同作用域进行链接后被调用,因此它负责设置事件监听器,监视数据变化和实时的操作DOM。
link函数对绑定了实时数据的DOM具有控制能力,因此需要考虑性能问题。在选择是用编译函数还是链接函数实现功能时,将性能影响考虑进去。
链接函数的签名如下:
link: function(scope, element, attrs) {
// 在这里操作DOM
}
如果指令定义中有require选项,函数签名中会有第四个参数,代表着所依赖的控制器 或者 指令的控制器
// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在这里操作DOM,可以访问required指定的控制器
}
ngMode
ngModel是一个用法特殊的指令,它提供更底层的API来处理控制器内的数据。
ngModel控制器会随ngModel被一直注入到指令中,其中包含了一些方法。为了访问ngModelController必须使用require设置(像前面的例子中那样):
angular.module('myApp') .directive('myDirective',function(){ return { require: '?ngModel', link: function(scope, ele, attrs, ngModel) { if (!ngModel) return; // 现在我们的指令中已经有ngModelController的一个实例 } }; });
如果不设置require选项, ngModelController就不会被注入到指令中。
这个指令没有隔离作用域。如果给这个指令设置隔离作用域,将导致内部ngModel无法更新外部ngModel的对应值: AngularJS会在本地作用域以外查询值。
设置作用域中的视图值,调用ngModel.$setViewValue()函数。 ngModel.$setViewValue()函数可以接受一个参数,参数是我们想要赋值给ngModel实例的实际值。这个方法会更新控制器上本地的$viewValue,然后将值传递给每一个$parser函数(包括验证器)。
当值被解析,且$parser流水线中所有的函数都调用完成后,值会被赋给$modelValue属性,并且传递给指令中ng-model属性提供的表达式。
所有步骤都完成后, $viewChangeListeners中所有的监听器都会被调用。注意,单独调用$setViewValue()不会唤起一个新的digest循环,因此如果想更新指令,需要在设置$viewValue后手动触发digest。
$setViewValue()方法适合于在自定义指令中监听自定义事件(比如使用具有回调函数的jQuery插件),我们会希望在回调时设置$viewValue并执行digest循环。
angular.module('myApp') .directive('myDirective', function() { return { require: '?ngModel', link: function(scope, ele, attrs, ngModel) { if (!ngModel) return; $(function() { ele.datepicker({ onSelect: function(date) { // 设置视图和调用apply scope.$apply(function() { ngModel.$setViewValue(date); }); } }); }); } }; });
ngModelController中有几个属性可以用来检查甚至修改视图:
1. $viewValue
$viewValue属性保存着更新视图所需的实际字符串。
2. $modelValue
$modelValue由数据模型持有。 $modelValue和$viewValue可能是不同的,取决于$parser流水线是否对其进行了操作
3. $parsers
$parsers的值是一个由函数组成的数组,其中的函数会以流水线的形式被逐一调用。ngModel从DOM中读取的值会被传入$parsers中的函数,并依次被其中的解析器处理
4. $formatters
$formatters的值是一个由函数组成的数组,其中的函数会以流水线的形式在数据模型的值发生变化时被逐一调用。它和$parser流水线互不影响,用来对值进行格式化和转换,以便在绑定了这个值的控件中显示。
5. $viewChangeListeners
$viewChangeListeners的值是一个由函数组成的数组,其中的函数会以流水线的形式在视图中的值发生变化时被逐一调用。通过$viewChangeListeners,可以在无需使用$watch的情况下实现类似的行为。由于返回值会被忽略,因此这些函数不需要返回值。
6. $error
$error对象中保存着没有通过验证的验证器名称以及对应的错误信息。
7. $pristine
$pristine的值是布尔型的,可以告诉我们用户是否对控件进行了修改。
8. $dirty
$dirty的值和$pristine相反,可以告诉我们用户是否和控件进行过交互。
9. $valid
$valid值可以告诉我们当前的控件中是否有错误。当有错误时值为false, 没有错误时值为true。
10. $invalid
$invalid值可以告诉我们当前控件中是否存在至少一个错误,它的值和$valid相反。
自定义验证:
angular.module('validationExample', []) .directive('ensureUnique',function($http) { return { require: 'ngModel', link: function(scope, ele, attrs, c) { scope.$watch(attrs.ngModel, function() { $http({ method: 'POST', url: '/api/check/' + attrs.ensureUnique, data: {field: attrs.ensureUnique, valud:scope.ngModel }).success(function(data,status,headers,cfg) { c.$setValidity('unique', data.isUnique); }).error(function(data,status,headers,cfg) { c.$setValidity('unique', false); }); }); } }; });
出于演示目的,尽管我们在指令内置入了一个$http调用,但是在产品中的指令内使用$http是不明智的。相反,将它置入到服务中会更好。关于服务的更多信息请参第14章。
<input type="text" placeholder="Desired username" name="username“ ng-model="signup.username"
ng-minlength="3" ng-maxlength="20" ensure-unique="username" required />
每当ngModel中对应的字段发生变化就会向服务器发送请求,以检查用户名是否是唯一的