学习Angular,首先要理解其做用域机制。html
Angular应用是分层的,主要有三个层面:视图,模型,视图模型。其中,视图很好理解,就是直接可见的界面,模型就是数据,那么视图模型是什么呢?是一种把数据包装给视图调用的东西。git
所谓做用域,也就是视图模型中的一个概念。github
在第一章中,有这么一个很简单的数据绑定例子:数组
<input ng-model="rootA"/> <div>{{rootA}}</div>
当时咱们解释过,这个例子可以运行的的缘由是,它的rootA变量被建立在根做用域上。每一个Angular应用默认有一个根做用域,也就是说,若是用户未指定本身的控制器,变量就是直接挂在这个层级上的。浏览器
做用域在一个Angular应用中是以树的形状体现的,根做用域位于最顶层,从它往下挂着各级做用域。每一级做用域上面挂着变量和方法,供所属的视图调用。架构
若是想要在代码中显式使用根做用域,能够注入$rootScope。app
怎么证明刚才的例子中,$rootScope确实存在,并且变量真的在它上面呢?咱们来写个代码:ide
function RootService($rootScope) { $rootScope.$watch("rootA", function(newVal) { alert(newVal); }); }
这时候咱们能够看到,这段代码并未跟界面产生任何关系,但里面的监控表达式确实生效了,也就是说,观测到了根做用域上rootA的变动,说明有人给它赋值了。函数
在开发过程当中,咱们可能会出现控制器的嵌套,看下面这段代码:学习
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> </div> </div> function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) { }
注意结果,咱们能够看到界面显示了两个1,而咱们只在OuterCtrl的做用域里定义了a变量,但界面给咱们的结果是,两个a都有值。这里内层的a值显然来自外层,由于当咱们对界面做出这样的调整以后,就只有一个了:
<div ng-controller="OuterCtrl"> <span>{{a}}</span> </div> <div ng-controller="InnerCtrl"> <span>{{a}}</span> </div>
这是为何呢?在Angular中,若是两个控制器所对应的视图存在上下级关系,它们的做用域就自动产生继承关系。什么意思呢?
先考虑在纯JavaScript代码中,两个构造函数各自有一个实例:
function Outer() { this.a = 1; } function Inner() { } var outer = new Outer(); var inner = new Inner();
在这里面添加什么代码,可以让inner.a == 1呢?
熟悉JavaScript原型的咱们,固然绝不犹豫就加了一句:Inner.prototype = outer;
function Outer() { this.a = 1; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner();
因而就获得想要的结果了。
再回到咱们的例子里,Angular的实现机制其实也就是把这两个控制器中的$scope做了关联,外层的做用域实例成为了内层做用域的原型。
以此类推,整个Angular应用的做用域,都存在自顶向下的继承关系,最顶层的是$rootScope,而后一级一级,沿着不一样的控制器往下,造成了一棵做用域的树,这也就像封建社会:天子高高在上,分茅裂土,公侯伯子男,一级一级往下,层层从属。
既然做用域是经过原型来继承的,天然也就能够推论出一些特征来。好比说这段代码,点击按钮的结果是什么?
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> <button ng-click="a=a+1">a++</button> </div> </div>
function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) { }
点了按钮以后,两个a不一致了,里面的变了,外面的没变,这是为何?原先两层不是共用一个a吗,怎么会出现两个不一样的值?看这句就能明白了,至关于咱们以前那个例子里,这样赋值了:
function Outer() { this.a = 1; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner(); inner.a = inner.a + 1;
最后这句,颇有意思,它有两个过程,取值的时候,由于inner自身上面没有,因此沿着原型往上取到了1,而后自增了以后,赋值给本身,这个赋值的时候就不一样了,敬爱的林副主席教导咱们:有a就赋值,没有a,创造一个a也要赋值。
因此这么一来,inner上面就被赋值了一个新的a,outer里面的仍然保持原样,这也就致使了刚才看到的结果。
初学者在这个问题上很容易犯错,若是不能随时很明确地认识到这些变量的差别,很容易写出有问题的程序。既然这样,咱们能够用一些别的方式来减小变量的歧义。
好比说,咱们就是想上下级共享变量,不建立新的,该怎么办呢?
考虑下面这个例子:
function Outer() { this.data = { a: 1 }; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner(); console.log(outer.data.a); console.log(inner.data.a); // 注意,这个时候会怎样? inner.data.a += 1; console.log(outer.data.a); console.log(inner.data.a);
此次的结果就跟上次不一样了,缘由是什么呢?由于二者的data是同一个引用,对这个对象上面的属性修改,是能够反映到两级对象上的。咱们经过引入一个data对象的方式,继续使用了原先的变量。把这个代码移植到AngularJS里,就变成了下面这样:
<div ng-controller="OuterCtrl"> <span>{{data.a}}</span> <div ng-controller="InnerCtrl"> <span>{{data.a}}</span> <button ng-click="data.a=data.a+1">increase a</button> </div> </div>
function OuterCtrl($scope) { $scope.data = { a: 1 }; } function InnerCtrl($scope) { }
从这个例子咱们就发现了,若是想要避免变量歧义,显式指定所要使用的变量会是比较好的方式,那么若是咱们确实就是要在上下级分别存在相同的变量该怎么办呢,好比说下级的点击,想要给上级的a增长1,咱们可使用$parent来指定上级做用域。
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> <button ng-click="$parent.a=a+1">increase a</button> </div> </div>
function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) { }
从Angular 1.2开始,引入了控制器实例的别名机制。在以前,可能都须要向控制器注入$scope,而后,控制器里面定义可绑定属性和方法都是这样:
function CtrlA($scope) { $scope.a = 1; $scope.foo = function() { }; }
<div ng-controller="CtrlA"> <div>{{a}}</div> <button ng-click="foo()">click me</button> </div>
其实$scope的注入是一个比较冗余的概念,没有必要把这种概念过度暴露给用户。在应用中出现的做用域,有的是充当视图模型,而有些则是处于隔离数据的须要,前者如ng-controller,后者如ng-repeat。在最近版本的AngularJS中,已经能够不显式注入$scope了,语法是这样:
function CtrlB() { this.a = 1; this.foo = function() { }; }
这里面,就彻底没有$scope的身影了,那这个控制器怎么使用呢?
<div ng-controller="CtrlB as instanceB"> <div>{{instanceB.a}}</div> <button ng-click="instanceB.foo()">click me</button> </div>
注意咱们在引入控制器的时候,加了一个as语法,给CtrlB的实例取了一个别名叫作instanceB,这样,它下属的各级视图均可以显式使用这个名称来调用其属性和方法,不易引发歧义。
在开发过程当中,为了不模板中的变量歧义,应当尽量使用命名限定,好比a.b,出现歧义的可能性就比单独的b要少得多。
在一个应用中,最多见的会建立做用域的指令是ng-controller,这个很好理解,由于它会实例化一个新的控制器,往里面注入一个$scope,也就是一个新的做用域,因此通常人都会很天然地理解这里面的做用域隔离关系。可是对于另一些状况,就有些困惑了,好比说,ng-repeat,怎么理解这个东西也会建立新做用域呢?
仍是看以前的例子:
$scope.arr = [1, 2, 3];
<ul> <li ng-repeat="item in arr track by $index">{{item}}</li> </ul>
在ng-repeat的表达式里,有一个item,咱们来思考一下,这个item是个什么状况。在这里,数组中有三个元素,在循环的时候,这三个元素都叫作item,这时候就有个问题,如何区分每一个不一样的item,可能咱们这个例子还不够直接,那改一下:
<div>outer: {{sum1}}</div> <ul> <li ng-repeat="item in arr track by $index"> {{item}} <button ng-click="sum1=sum1+item">increase</button> <div>inner: {{sum1}}</div> </li> </ul>
这个例子运行一下,咱们会发现每一个item都会独立改变,说明它们确实是区分开了的。事实上,Angular在这里为ng-repeat的每一个子项都建立了单独的做用域,因此,每一个item都存在于本身的做用域里,互不影响。有时候,咱们是须要在循环内部访问外层变量的,回忆一下,在本章的前面部分中,咱们举例说,若是两个控制器,它们的视图有包含关系,内层控制器的做用域能够经过$parent来访问外层控制器做用域上的变量,那么,在这种循环里,是否是也能够如此呢?
看这个例子:
<div>outer: {{sum2}}</div> <ul> <li ng-repeat="item in arr track by $index"> {{item}} <button ng-click="$parent.sum2=sum2+item">increase</button> <div>inner: {{sum2}}</div> </li> </ul>
果真是能够的。不少时候,人们会把$parent误认为是上下两级控制器之间的访问通道,但从这个例子咱们能够看到,并不是如此,只是两级做用域而已,做用域跟控制器仍是不一样的,刚才的循环能够说是有两级做用域,但都处于同一个控制器之中。
刚才咱们已经提到了ng-controller和ng-repeat这两个经常使用的内置指令,二者都会建立新的做用域,除此以外,还有一些其余指令也会建立新的做用域,不少初学者在使用过程当中很容易产生困扰。
第一章咱们提到用ng-show和ng-hide来控制某个界面块的总体展现和隐藏,但一样的功能其实也能够用ng-if来实现。那么这二者的差别是什么呢,所谓show和hide,你们很好理解,就是某个东西原先有,只是控制是否显式,而if的含义是,若是知足条件,就建立这块DOM,不然不建立。因此,ng-if所控制的界面块,只有条件为真的时候才会存在于DOM树中。
除此以外,二者还有个差别,ng-show和ng-hide是不自带做用域的,而ng-if则本身建立了一级做用域。在用的时候,二者就是有差异的,好比说内部元素访问外层定义的变量,就须要使用相似ng-repeat那样的$parent语法了。
类似的类型还有ng-switch,ng-include等等,规律能够总结,也就是那些会动态建立一块界面的东西,都是自带一级做用域。
通常而言,在Angular工程中,基本是不须要手动建立做用域的,但真想建立的话,也是能够作到的。在任意一个已有的做用域上调用$new(),就能建立一个新的做用域:
var newScope = scope.$new();
刚建立出来的做用域是一个“悬空”的做用域,也就是说,它跟任何界面模板都不存在绑定关系,建立它的做用域会成为它的$parent。这种做用域能够通过$compile阶段,与某视图模板进行融合。
为了帮助理解,咱们能够用DocumentFragment做类比,看成用域被建立的时候,就比如是建立了一个DocumentFragment,它是不在DOM树上的,只有当它被append到DOM树上,才可以被当作普通的DOM来使用。
那么,悬空的做用域是否是什么用处都没有呢?也不是,尽管它未与视图关联,可是它的一些方法仍然能够用。
咱们在第一章里提到了$watch,这就是定义在做用域原型上的。若是咱们想要监控一个数据的变化,但这个数据并不是绑定到界面上的,好比下面这样,怎么办?
function IsolateCtrl($scope) { var child = { a: 1 }; child.a++; }
注意这个child,它并未绑定到$scope上,若是咱们想要在a变化的时候作某些事情,是没有办法作的,由于直到最近的某些浏览器中,才实现了Object.observe这样的对象变动观测方法,以前某些浏览器中要作这些,会比较麻烦。
可是咱们的$watch和$eval之类的方法,其实都是实如今做用域对象上的,也就是说,任何一个做用域,即便没有与界面产生关联,也是可以使用这些方法的。
function IsolateCtrl($scope) { var child = $scope.$new(); child.a = 1; child.$watch("a", function(newValue) { alert(newValue); }); $scope.change = function() { child.a++; }; }
这时候child里面a的变动就能够被观测到,而且,这个child只有本做用域能够访问到,至关因而一个加强版的数据模型。若是咱们要作一个小型流程引擎之类的东西,做用域对象上提供的这些方法会颇有用。
咱们刚才提到使用$parent来处理上下级的通信,但其实这不是一种好的方式,尤为是在不一样控制器之间,这会增长它们的耦合,对组件复用很不利。那怎样才能更好地解耦呢?咱们可使用事件。
提到事件,可能不少人想到的都是DOM事件,其实DOM事件只存在于上层,并且没有业务含义,若是咱们想要传递一个明确的业务消息,就须要使用业务事件。这种所谓的业务事件,其实就是一种消息的传递。
假设有如图所示的应用:
这张图中有一个应用,下面存在两个视图块A和B,它们分别又有两个子视图。这时候,若是子视图A1想要发出一个业务事件,使得B1和B2可以获得通知,过程就会是:
刚才的图形体现了界面的包含关系,若是把这个图再立体化,就会是下面这样:
对于这种事件的传播方式,能够有个相似的比喻:
好比说,某军队中,1营1连1排长想要给1营2连下属的三个排发个警惕通知,他的通知方向是一级一级向上汇报,直到双方共同的上级,也就是1营指挥人员这里,而后再沿着二连这个路线向下去通知。
$scope.$emit("someEvent", {});
$scope.$broadcast("someEvent", {});
这两个方法的第二个参数是要随事件带出的数据。
注意,这两种方式传播事件,事件的发送方本身也会收到一份。
使用事件的主要做用是消除模块间的耦合,发送方是不须要知道接收方的情况的,接收方也不须要知道发送方的情况,双方只须要传送必要的业务数据便可。
不管是$emit仍是$broadcast发送的事件,均可以被接收,接收这两种事件的方式是同样的:
$scope.$on("someEvent", function(e) { // 这里从e上能够取到发送过来的数据 });
注意,事件被接收了,并不表明它就停止了,它仍然会沿着原来的方向继续传播,也就是:
有时候,咱们但愿某一级收到事件以后,就让它停下来,再也不传播,能够把事件停止。这时候,两种事件的区别就体现出来了,只有$emit发出的事件是能够被停止的,$broadcast发出的不能够。
若是想要阻止$emit事件的继续传播,能够调用事件对象的stopPropagation()方法。
$scope.$on("someEvent", function(e) { e.stopPropagation(); });
可是,想要阻止$broadcast事件的传播,就麻烦了,咱们只能经过变通的方式:
首先,调用事件对象的preventDefault()方法,而后,在收取这个事件对象的时候,判断它的defaultPrevented属性,若是为true,就忽略此事件。这个过程比较麻烦,其实咱们通常是不须要管的,只要不监听对应的事件就能够了。在实际使用过程当中,也应当尽可能少使用事件的广播,尤为是从较高的层级进行广播。
上级做用域
$scope.$on("someEvent", function(e) { e.preventDefault(); });
下级做用域
$scope.$on("someEvent", function(e) { if (e.defaultPrevented) { return; } });
在Angular中,不一样层级做用域之间的数据通讯有多种方式,能够经过原型继承的一些特征,也能够收发事件,还可使用服务来构造单例对象进行通讯。
前面提到的这个军队的例子,有些时候沟通效率比较低,特别是层级多的时候。想象一下,刚才这个只有三层,若是更复杂,一个排长的消息都必定要报告到军长那边再下发到其余基层主官,一定贻误军情,更况且有不少下级根本不须要知道这个消息。
那怎么办呢,难道是直接打电话沟通吗?这个效率高是高,就是容易乱,这也就至关于界面块之间的直接经过id调用。
Angular的做用域树相似于传统的组织架构树,一个大型企业,通常都会有若干层级,近年来有不少管理的方法论,好比说组织架构的扁平化。
咱们能不能这样:搞一个专门负责通信的机构,你们的消息都发给它,而后由它发给相关人员,其余人员在理念上都是平级关系。
这就是一个很典型的订阅发布模式,接收方在这里订阅消息,发布方在这里发布消息。这个过程能够用这样的图形来表示:
代码写起来也很简单,把它作成一个公共模块,就能够被各类业务方调用了:
app.factory("EventBus", function() { var eventMap = {}; var EventBus = { on : function(eventType, handler) { //multiple event listener if (!eventMap[eventType]) { eventMap[eventType] = []; } eventMap[eventType].push(handler); }, off : function(eventType, handler) { for (var i = 0; i < eventMap[eventType].length; i++) { if (eventMap[eventType][i] === handler) { eventMap[eventType].splice(i, 1); break; } } }, fire : function(event) { var eventType = event.type; if (eventMap && eventMap[eventType]) { for (var i = 0; i < eventMap[eventType].length; i++) { eventMap[eventType][i](event); } } } }; return EventBus; });
事件订阅代码:
EventBus.on("someEvent", function(event) { // 这里处理事件 var c = event.data.a + event.data.b; });
事件发布代码:
EventBus.fire({ type: "someEvent", data: { aaa: 1, bbb: 2 } });
注意,若是在复杂的应用中使用事件总线,须要慎重规划事件名,推荐使用业务路径,好比:"portal.menu.selectedMenuChange",以免事件冲突。
在本章,咱们学习了做用域相关的知识,以及它们之间传递数据的方式。做用域在整个Angular应用中造成了一棵树,以$rootScope为根部,开枝散叶。这棵树独立于DOM而存在,又与DOM相关联。事件在整个树上传播,如蜂飞蝶舞。
整体来讲,使用AngularJS对JavaScript的基本功是有必定要求的,由于这里面大部分实现都依赖于纯JavaScript语法,好比原型继承的使用。若是对这一块有充分的认识,理解Angular的做用域就会比较容易。
一个大型单页应用,须要对部件的整合方式和通讯机制做良好的规划,为它们创建良好的秩序,这对于确保整个应用的稳定性是很是必要的。
首要问题不是自由,而是创建合法的公共秩序。人类能够无自由而有秩序,但不能无秩序而有自由。——缪尔·亨廷顿
本章所涉及的全部Demo,参见在线演示地址
演讲幻灯片下载:点这里