作用域的嵌套及与元素的关联
作用域是控制器与视图,指令与视图之间的粘合剂。
作用域可以做哪些事情:
- 作用域提供了 ($watch) 方法监听数据模型的变化
- 作用域提供了 ($apply) 方法把不是由Angular触发的数据模型的改变引入Angular的控制范围内(如控制器,服务,及Angular事件处理器等)
- 作用域提供了基于原型链继承其父作用域属性的机制,就算是嵌套于独立的应用组件中的作用域也可以访问共享的数据模型(这个涉及到指令间嵌套时作用域的几种模式)
- 作用域提供了表达式的执行环境,比如像 {{username}} 这个表达式,必须得是在一个拥有username这个属性的作用域中执行才会有意义,也就是说,作用域中可能会像这样 scope.username 或是 $scope.username,至于有没有 $ 符号,看你是在哪里访问作用域了
作用域的分类:根作用域($rootScope)、父作用域、子作用域、独立作用域(存在于自定义指令中)。
AngularJS存在四种作用域:
- 普通的带原型继承的作用域 -- ng-include, ng-switch, ng-controller, ng-if,directive with scope: true等;(还有ng-view,但是一般路由都使用ui-router,而不使用AngularJS自带的路由模块)
- 普通的带原型继承的,并且有赋值行为的作用域 -- ng-repeat,ng-repeat为每一个迭代项创建一个普通的有原型继承的子作用域,但同时在子作用域中创建新属性存储迭代项;
- “Isolate”作用域(独立作用域) -- directive with scope: {...}, 该作用域没有原型继承,但可以通过'=', '@', 和 '&'与父作用域通信。
- “transcluded”作用域 -- directive with transclude: true,它也是普通的带原型继承的作用域,但它与“Isolate”作用域是相邻的好基友。
(这里不讨论自定义指令的作用域).
在AngularJS中,一个scope跟一个元素关联,而一个元素不是必须直接跟一个scope关联。元素通过以下三种方式被分配一个scope:
- scope通过controller或者directive创建在一个element上(注意,指令不总是引入新的scope)
- 如果一个元素上不存在scope,那么这个元素将继承他的父级scope(更准确的说是直接使用父级作用域中的数据模型)
- 如果一个元素不是某个ng-app的一部分,那么它不属于任何AngularJS中的 scope
在AngularJS的上下文中查看某一元素上分配的scope:
打开一个AngularJS项目,打开控制台,选中你想要查看的那个元素,然后在控制台中输入以下代码:
angular.element($0).scope();
scope的一些内置属性:
- $id scope 的唯一标识
- $root 根scope
- $parent 父级scope, 如果 scope == scope.$root 则为 null
- $$childHead 第一个子 scope, 如果没有则为 null
- $$childTail 最后一个子scope, 如果没有则为 null
- $$prevSibling 前一个相邻节点 scope, 如果没有则为 null
- $$nextSibling 下一个相邻节点 scope, 如果没有则为 null
下面是例子
ng-include – 创建子作用域
html代码(这里要注意ng-include的两种写法):
<!DOCTYPE html> <html lang="en" ng-app="scopeApp"> <head> <meta charset="UTF-8"> <title>Scope</title> <script src="js/angular.js"></script> </head> <body> <!--ng-include--> <div ng-controller="fifthCtrl"> {{name}} <div ng-include="'tpls/include-0.html'"></div> <script type="text/ng-template" id="tpls/include-0.html"> <p>this is a test {{name}}</p> <input type="text" ng-model="name"/> <input type="text" ng-model="$parent.name"/> </script> </div> <script src="script/app.js"></script> </body> </html>
js代码:
var directiveApp = angular.module('directiveApp',[]); directiveApp.controller('fifthCtrl',['$rootScope','$scope',function($rootScope,$scope) { $scope.name = 'thie fifth'; }]);
ng-repeat – 创建子作用域
html代码:
<!DOCTYPE html> <html lang="en" ng-app="directiveApp"> <head> <meta charset="UTF-8"> <title>Directive</title> <script src="js/angular.js"></script> </head> <body> <!--ng-repeat--> <div ng-controller="sixedCtrl"> <h3>显示arr</h3> <p>{{arr[0]}}</p> <p>{{arr[1]}}</p> <p>{{arr[2]}}</p> <h3>显示brr</h3> <p>{{brr[0].name}}</p> <p>{{brr[1].name}}</p> <p>{{brr[2].name}}</p> <ol> <li ng-repeat="item in arr"> <input type="text" ng-model="item"/> </li> </ol> <ol> <li ng-repeat="item in brr"> <input type="text" ng-model="item.name"/> </li> </ol> </div> <script src="script/app.js"></script> </body> </html>
js代码:
var directiveApp = angular.module('directiveApp',[]); directiveApp.controller('sixedCtrl',['$rootScope','$scope',function($rootScope,$scope) { $scope.arr = ['one','two','three']; $scope.brr = [{name:'jack',age:1},{name:'hana',age:4},{name:'bill',age:6}]; }]);
作用域的分层结构
在前面提到过,在AngularJS中一个scope与一个元素关联,而元素是有嵌套结构的,所以AngularJS中的scope也是有嵌套结构的(外层的作用域实例成为了内层作用域的原型),下面用一个例子来说明这种嵌套结构。
HTML代码:
<!DOCTYPE html> <html lang="en" ng-app="scopeLevel"> <head> <meta charset="UTF-8"> <title>Title</title> <style type="text/css"> .red-border{ border:1px solid red; margin-bottom: 10px; padding:20px 20px; } </style> <script src="js/angular.js"></script> </head> <body> <div class="red-border" ng-controller="fatherCtrl"> <h3>I am {{name}}</h3> <div class="red-border" ng-controller="childCtrl"> <h3>I am {{name}}</h3> <ol> <li class="red-border" ng-repeat="item in arr">I am {{item.name}}</li> </ol> </div> </div> <div class="red-border" ng-controller="brotherCtrl"> <h3>I am {{name}}</h3> </div> <script src="script/scope-1.js"></script> </body> </html>
js代码:
var scopeLevel = angular.module('scopeLevel',[]); scopeLevel.controller('fatherCtrl',['$rootScope','$scope',function($rootScope,$scope) { $scope.name = 'Father'; }]); scopeLevel.controller('childCtrl',['$rootScope','$scope',function($rootScope,$scope) { $scope.arr = [{name:'one'},{name:'two'},{name:'three'}]; $scope.name = 'Child'; }]); scopeLevel.controller('brotherCtrl',['$rootScope','$scope',function($rootScope,$scope) { $scope.name = 'brother'; }]);
下图是例子中作用域的嵌套结构示意图:
下图是例子中作用域与HTML元素的关联情况示意图:
基于作用域的事件传播
作用域可以像DOM节点一样,进行事件的传播。主要有两个方法:
- $broadcast:从父级作用域广播至子级作用域
- $emit:从子级作用域往上发射到父级作用域
上述两个方法发布的事件都是通过作用域 的 $on 方法进行监听的。
下面是例子:
例子的目录结构:
index.html文件
<!DOCTYPE html> <html lang="en" ng-app="scopeEventApp"> <head> <meta charset="UTF-8"> <script src="../js/angular.js"></script> <script src="../js/angular-ui-router.js"></script> <title>基于作用域的事件传递</title> </head> <body> <a ui-sref="viewone">视图-1</a> <a ui-sref="viewtwo">视图-2</a> <button ng-click="broadcastEvent()" type="button" value="click">click to broadcast</button> <div ui-view></div> <script src="js/controller.js"></script> </body> </html>
view_one.html文件
<h1>this is {{name}}</h1> <a ui-sref="viewone.child">child view</a> <button ng-click="emitEvent()" type="button" value="click me">click</button> <div ui-view></div>
view_one_child.html文件
<h1>this is {{$parent.name}}' child.</h1> <button ng-click="childEmitEvent()" type="button" value="click me">child click</button>
view_two.html文件
<h1>this is {{name}}</h1> <button ng-click="emitEvent()" type="button" value="click me">click</button>
controller.js文件
1 var scopeEventApp = angular.module('scopeEventApp',['ui.router']); 2 //订阅/发布服务 3 //不同控制器中使用同一个服务,其实使用的是同一个对象 4 //服务中的代码是从dropzone插件中抠出来的,稍微修改了一下 5 scopeEventApp.factory('Emitter',function() { 6 var eventMap = {}; 7 var Emitter = {}; 8 Emitter.__slice = [].slice; 9 Emitter.__hasProp = {}.hasOwnProperty; 10 //事件绑定函数 11 Emitter.on = function(event, fn) { 12 eventMap = eventMap || {}; 13 if (!eventMap[event]) { 14 eventMap[event] = []; 15 } 16 eventMap[event].push(fn); 17 //return this; 18 }; 19 //触发事件的函数 20 Emitter.emit = function() { 21 var args, callback, callbacks, event, _i, _len; 22 //emit函数的第一个参数是事件的名字,保存在event变量中 23 event = arguments[0]; 24 //如果arguments.length>=2, 25 // 则args=__slice.call(arguments, 1);否则args=[] 26 args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 27 28 eventMap = eventMap || {}; 29 //现在callbacks是一个数组, 30 // 数组中的每一项是事件处理程序(本质上是一个函数) 31 callbacks = eventMap[event]; 32 33 if (callbacks) { 34 //将callbacks中的每一个事件处理程序都调用一次 35 for (_i = 0, _len = callbacks.length; _i < _len; _i++) { 36 callback = callbacks[_i]; 37 callback.apply(this, args); 38 } 39 } 40 //return this; 41 }; 42 //解绑事件的函数 43 Emitter.off = function(event, fn) { 44 var callback, callbacks, i, _i, _len; 45 //如果没有_callbacks属性或者没有传参数, 46 // 则添加_callbacks属性,初始化为一个空对象 47 if (!eventMap || arguments.length === 0) { 48 eventMap = {}; 49 return; 50 } 51 //如果有_callbacks属性且传了至少一个参数, 52 // 那么callbacks的值为对应事件名字下的所有事件处理程序的集合 53 //注意callbacks变量的值可能是undefined 54 callbacks = eventMap[event]; 55 //如果callbacks的值是undefined,那么返回该实例对象 56 if (!callbacks) { 57 return; 58 } 59 //如果只传了一个参数,即事件名字, 60 // 那么删除_callbacks对象中对应的键,并返回该实例对象 61 if (arguments.length === 1) { 62 delete eventMap[event]; 63 return; 64 } 65 //如果传入了两个参数,第二个参数是该事件中的一个事件处理程序的名字, 66 // 那么就将该事件处理程序从该事件对应的事件处理程序集合中的删除 67 for (i = _i = 0, _len = callbacks.length; _i < _len; i = ++_i) { 68 callback = callbacks[i]; 69 if (callback === fn) { 70 callbacks.splice(i, 1); 71 break; 72 } 73 } 74 //return this; 75 }; 76 return Emitter; 77 78 }); 79 scopeEventApp.run(function($rootScope,Emitter) { 80 $rootScope.name = 'rootScope'; 81 //向子级作用域广播事件 82 $rootScope.broadcastEvent = function() { 83 console.log($rootScope.$broadcast); 84 $rootScope.$broadcast('rootEvent',{"name":"$rootScope"}); 85 }; 86 //监听视图1发射的事件 87 $rootScope.$on("viewonetest",function(e) { 88 console.log(e); 89 alert(e.name); 90 }); 91 //监听视图2发射的事件 92 $rootScope.$on('viewonechildtest',function(e) { 93 console.log(e); 94 alert(e.name + ' ' + $rootScope.name); 95 }); 96 }); 97 //路由 98 scopeEventApp.config(function($stateProvider) { 99 $stateProvider.state('viewone',{ 100 url:'/viewone', 101 templateUrl:'tpls/view_one.html', 102 controller:'viewOneCtrl' 103 }); 104 $stateProvider.state('viewone.child',{ 105 views:{ 106 "@viewone":{ 107 url:'/viewone.child', 108 templateUrl:'tpls/view_one_child.html', 109 controller:'viewOneChildCtrl' 110 } 111 } 112 }); 113 $stateProvider.state('viewtwo',{ 114 url:'/viewtwo', 115 templateUrl:'tpls/view_two.html', 116 controller:'viewTwoCtrl' 117 }); 118 }); 119 //控制器 120 scopeEventApp.controller('viewOneCtrl', function($scope,Emitter) { 121 $scope.name = "view one"; 122 //监听父级作用域广播的事件 123 $scope.$on('rootEvent',function(e) { 124 console.log(e); 125 alert(e.name + ' --- ' + $scope.name + '监听'); 126 }); 127 //向上发射事件 128 $scope.emitEvent = function($event) { 129 $scope.$emit('viewonetest',{name:'viewOne'}); 130 Emitter.emit('test'); 131 console.log(Emitter.name); 132 }; 133 //监听子视图发射的事件 134 $scope.$on('viewonechildtest',function(e) { 135 console.log(e); 136 alert(e.name + ' --- ' + $scope.name); 137 //e.stopPropagation();//阻止事件继续向上级作用域传播 138 }); 139 }); 140 scopeEventApp.controller('viewTwoCtrl', function($scope,Emitter) { 141 $scope.name = "view two"; 142 //监听父级作用域广播的事件 143 $scope.$on('rootEvent',function(e) { 144 console.log(e); 145 alert(e.name + ' --- ' + $scope.name + '监听'); 146 }); 147 //监听视图1的子视图发射的事件---实际上是监听不到的 148 $scope.$on('viewonechildtest',function(e) { 149 console.log(e); 150 alert(e.name + ' --- ' + $scope.name); 151 }); 152 //订阅/发布服务的测试 153 $scope.emitEvent = function($event) { 154 Emitter.emit('test'); 155 }; 156 }); 157 scopeEventApp.controller('viewOneChildCtrl', function($scope,Emitter) { 158 $scope.name = "view one child"; 159 //监听父级作用域广播的事件 160 $scope.$on('rootEvent',function(e) { 161 console.log(e); 162 alert(e.name + ' --- ' + $scope.name + '监听'); 163 }); 164 //视图1的子视图向上发射事件 165 $scope.childEmitEvent = function() { 166 $scope.$emit('viewonechildtest',{name:'viewOneChild'}); 167 }; 168 //在视图1的子视图的控制器中用订阅/发布服务绑定一个事件 169 Emitter.name = 'lonely'; 170 Emitter.on('test',function() { 171 console.log('this is a test'); 172 }); 173 });
例子中事件传播的示意图:
使用$broadcast和$emit发布事件时,传入这两个方法的第一个参数是要发布的事件的名字,第二个是一个对象,是要随事件发布的数据,在使用$on方法监听事件的时候通过事件处理程序的事件对象可以访问到随事件一起发布的数据。
通过作用域的$on方法可以监听在作用域上传播的事件,那么要怎样阻止事件在作用域上的传播呢?
要注意的是,只有$emit发出的事件是可以被中止的,$broadcast发出的不可以。
如果要阻止$emit发布的事件的继续传播,直接调用事件对象的stopPropagation()方法即可。
代码:
$scope.$on('eventName',function(e) { e.stopPropagation(); });
对于$broadcast发布的事件,没有直接阻止其传播的方法可供调用,不过只要不去监听,就可以无视其发布的事件。
下面是在其他博主的文章中介绍的另一种方法,不过本质上并没有阻止事件的传播,所以最好的方法还是不去监听就好。
代码如下:
上级作用域:
$scope.$on('rootEvent',function(e) { e.preventDefault(); });
下级作用域:
$scope.$on('rootEvent',function(e) { if (e.defaultPrevented) { return; } });
最后要说的一点是,在例子中有一个关于订阅/发布模式的服务Emitter,在例子中的所有控制器中都用了这个服务。在视图1的子视图中用Emitter服务发布了test事件,并给Emitter添加了一个name属性,然后在视图1和视图2 以及根视图上都通过点击事件触发这个test事件并输出Emitter的name属性,从结果来看,不同控制器引用同一个服务时,实际上他们引用的是同一个对象。
以上是关于AngularJS作用域的基本知识。
参考文章:
2. 何为作用域(Scope)