AngularJS----做用域

做用域的嵌套及與元素的關聯

做用域是控制器與視圖,指令與視圖之間的粘合劑。css

做用域能夠作哪些事情:html

  • 做用域提供了 ($watch) 方法監聽數據模型的變化
  • 做用域提供了 ($apply) 方法把不是由Angular觸發的數據模型的改變引入Angular的控制範圍內(如控制器,服務,及Angular事件處理器等)
  • 做用域提供了基於原型鏈繼承其父做用域屬性的機制,就算是嵌套於獨立的應用組件中的做用域也能夠訪問共享的數據模型(這個涉及到指令間嵌套時做用域的幾種模式)
  • 做用域提供了表達式的執行環境,好比像 {{username}} 這個表達式,必須得是在一個擁有username這個屬性的做用域中執行纔會有意義,也就是說,做用域中可能會像這樣 scope.username 或是 $scope.username,至於有沒有 $ 符號,看你是在哪裏訪問做用域了

做用域的分類:根做用域($rootScope)、父做用域、子做用域、獨立做用域(存在於自定義指令中)。angularjs

AngularJS存在四種做用域:數組

  1. 普通的帶原型繼承的做用域 -- ng-include, ng-switch, ng-controller, ng-if,directive with scope: true等;(還有ng-view,可是通常路由都使用ui-router,而不使用AngularJS自帶的路由模塊)
  2. 普通的帶原型繼承的,而且有賦值行爲的做用域 -- ng-repeat,ng-repeat爲每個迭代項建立一個普通的有原型繼承的子做用域,但同時在子做用域中建立新屬性存儲迭代項;
  3. 「Isolate」做用域(獨立做用域) -- directive with scope: {...}, 該做用域沒有原型繼承,但能夠經過'=', '@', 和 '&'與父做用域通訊。
  4. 「transcluded」做用域 -- directive with transclude: true,它也是普通的帶原型繼承的做用域,但它與「Isolate」做用域是相鄰的好基友。

(這裏不討論自定義指令的做用域).app

在AngularJS中,一個scope跟一個元素關聯,而一個元素不是必須直接跟一個scope關聯。元素經過如下三種方式被分配一個scope:ide

  •  scope經過controller或者directive建立在一個element上(注意,指令不老是引入新的scope)
  • 若是一個元素上不存在scope,那麼這個元素將繼承他的父級scope(更準確的說是直接使用父級做用域中的數據模型)
  • 若是一個元素不是某個ng-app的一部分,那麼它不屬於任何AngularJS中的 scope

在AngularJS的上下文中查看某一元素上分配的scope:函數

打開一個AngularJS項目,打開控制檯,選中你想要查看的那個元素,而後在控制檯中輸入如下代碼:post

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

下面是例子ui

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 });
View Code

例子中事件傳播的示意圖:

使用$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做用域的基本知識。

 

參考文章:

1. Angular之做用域與事件(轉)

2. 何爲做用域(Scope)

3. 理解AngularJS的做用域Scope

相關文章
相關標籤/搜索