在Rails等传统Web框架中,控制器将多个模型中的数据和模板组合在一块儿造成视图,并将其提供给用户,这个组合过程会产生一个单向视图。
AngularJS则采用了彻底不一样的解决方案。它建立实时模板来代替视图,而不是将数据合并进模板以后更新DOM。任何一个独立视图组件中的值都是动态替换的。这个功能能够说是AngularJS中最重要的功能之一
javascript
ng-app属性声明全部被其包含的内容都属于这个AngularJS应用,只有被具备ng-app属性的DOM元素包含的元素才会受AngularJS影响。
AngularJS会记录数据模型所包含的数据在任何特定时间点的值,java
当AngularJS认为某个值可能发生变化时,它会运行本身的事件循环来检查这个值是否变“脏”。若是该值从上次事件循环运行以后发生了变化,则该值被认为是“脏”值。这也是Angular能够跟踪和响应应用变化的方式
这个事件循环会调用$digest()循环,这个过程被称做脏检查(dirty checking) 。脏检查是检查数据模型变化的有效手段, AngularJS会在事件循环时执行脏检查(第24章会深刻讨论)来保证数据的一致性。
为了表示内部和内置的库函数,Angular使用$预约义对象。尽管这相似于全局的jQuery对象$,但它们是彻底无关的。只要遇到$符号,你均可以只把它看做一个Angular对象。express
使用ng-model指令将内部数据模型对象($scope)中的name属性绑定到了文本输入字段上,不管在文本输入字段中输入了什么,都会同步到数据模型中。$scope对象是一个简单的JavaScript对象,其中的属性能够被视图访问,也能够同控制器进行交互。api
<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); }]);
做用域是视图和控制器之间的胶水。视图中的模板会和做用域进行链接,应用会对DOM进行设置以便将属性变化通知给AngularJS。
做用域是应用状态的基础。基于动态绑定,能够依赖视图在修改数据时马上更新$scope,也能够依赖$scope在其发生变化时马上从新渲染视图。安全
AngularJS将$scope设计成和DOM相似的结构,所以$scope能够进行嵌套,也就是说咱们能够引用父级$scope中的属性。
服务器
做用域提供了监视数据模型变化的能力。它容许开发者使用其中的apply机制,将数据模型的变化在整个应用范围内进行通知。架构
AngularJS启动并生成视图时,会将根ng-app元素同$rootScope进行绑定。 $rootScope是全部$scope对象的最上层。app
$rootScope是AngularJS中最接近全局做用域的对象,能够看做是 根做用域,在$rootScope上附加太多业务逻并非好主意,这与污染JavaScript的全局做用域是同样的。
将应用的业务逻辑都放在控制器中,而将相关的数据都放在控制器的做用域中,这是很是完美的架构。
$scope对象就是一个普通的JavaScript对象,咱们能够在其上随意修改或添加属性。
$scope对象在AngularJS中充当数据模型,$scope并不负责处理和操做数据,
有如下的基本功能:
提供观察者以监视数据模型的变化;
能够将数据模型的变化通知给整个应用,甚至是系统外的组件;
能够进行嵌套,隔离业务功能和数据;
给表达式提供运算时所需的执行环境。
当Angular关心的事件发生在浏览器中时,好比用户在经过ng-model属性监控的输入字段中输入,或者带有ng-click属性的按钮被点击时, Angular的事件循环都会启动
每当事件被处理时, $scope就会对定义的表达式求值。此时事件循环会启动,而且Angular应用会监控应用程序内的全部对象,脏值检测循环也会运行。
$scope对象的生命周期处理有四个不一样阶段。
建立控制器或指令时, AngularJS会用$injector建立一个新的做用域,并在这个新建的控制器或指令运行时将做用域传递进去。
当Angular开始运行时,全部的$scope对象都会附加或者连接到视图中。全部建立$scope对象的函数也会将自身附加到视图中。这些做用域将会注册当Angular应用上下文中发生变化时须要运行的函数。
这些函数被称为$watch函数, Angular经过这些函数获知什么时候启动事件循环。
当事件循环运行时,它一般执行在顶层$scope对象上(被称做$rootScope),每一个子做用域都执行本身的脏值检测。每一个监控函数都会检查变化。若是检测到任意变化, $scope对象就会触
发指定的回调函数。
当一个$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会遍历整个HTML文档并根据JavaScript中的指令定义来处理页面上声明的指令。
每个指令的模板中均可能含有另一个指令,另一个指令也可能会有本身的模板。当AngularJS调用HTML文档根部的指令时,会遍历其中全部的模板,模板中也可能包含带有模板的指令。
模板树可能又大又深,尽管元素能够被多个指令所支持或修饰,这些指令自己的模板中也能够包含其余指令,但只有属于最高优先级指令的模板会被解析并添加到模板树中。
将包含模板的指令和添加行为的指令分离开来。
一旦对指令和其中的子模板进行遍历或编译,编译后的模板会返回一个叫作模板函数的函数。咱们有机会在指令的模板函数被返回前,对编译后的DOM树进行修改。
以ng-repeat为例,它会遍历指定的数组或对象,在数据绑定以前构建出对应的DOM结构。 只会有不多的性能开销。
一个指令的表现一旦编译完成,立刻就能够经过编译函数对其进行访问,编译函数的签名包含有访问指令声明所在的元素(tElemente)及该元素其余属性(tAttrs)的方法。这个编译函数返回前面提到的模板函数,其中含有完整的解析树。
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函数建立能够操做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指定的控制器
}
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中对应的字段发生变化就会向服务器发送请求,以检查用户名是不是惟一的