前言: 上篇博文AngularJs之directive中說了Scope作用域是個大坑,所以拿出來作為重點總結! 什麼是scope AngularJS 中,作用域是一個指嚮應用模型的對象,它是表達式的執行環境。作用域有層次結構,這個層次和相應的 DOM 幾乎是一樣的。作用域能監控表達式和傳遞事件。 在 ...
前言:
上篇博文AngularJs之directive中說了Scope作用域是個大坑,所以拿出來作為重點總結!
什麼是scope
AngularJS 中,作用域是一個指嚮應用模型的對象,它是表達式的執行環境。作用域有層次結構,這個層次和相應的 DOM 幾乎是一樣的。作用域能監控表達式和傳遞事件。
在 HTML 代碼中,一旦一個 ng-app 指令被定義,那麼一個作用域就產生了,由 ng-app 所生成的作用域比較特殊,它是一個根作用域($rootScope),它是其他所有$Scope 的最頂層。
除了用 ng-app 指令可以產生一個作用域之外,其他的指令如 ng-controller,ng-repeat 等都會產生一個或者多個作用域。此外,還可以通過 AngularJS 提供的創建作用域的工廠方法來創建一個作用域。這些作用域都擁有自己的繼承上下文,並且根作用域都為$rootScope。
在生成一個作用域之後,在編寫 AngularJS 代碼時,$scope 對象就代表了這個作用域的數據實體,我們可以在$scope 內定義各種數據類型,之後可以直接在 HTML 中以 {{變數名}} 方式來讓 HTML 訪問到這個變數。
繼承作用域
AngularJS 在創建一個作用域時,會檢索上下文,如果上下文中已經存在一個作用域,那麼這個新創建的作用域就會以 JavaScript 原型繼承機制繼承其父作用域的屬性和方法。
一些 AngularJS 指令會創建新的子作用域,並且進行原型繼承: ng-repeat、ng-include、ng-switch、ng-view、ng-controller, 用 scope: true 和 transclude: true 創建的 directive。
以下 HTML 中定義了三個作用域,分別是由 ng-app 指令所創建的$rootScope,parentCtrl 和 childCtrl 所創建的子作用域,這其中 childCtrl 生成的作用域又是 parentCtrl 的子作用域。
示例一:作用域的繼承實例
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script type="text/javascript">
angular.module('app', [])
.controller('parentCtrl', ['$scope', function($scope) {
$scope.args= 'Nick DeveloperWorks';
}])
.controller('childCtrl', ['$scope', function($scope) {
$scope.args= 'Nick DeveloperWorks for test';
}]);
</script>
<body ng-app="app">
<div ng-controller="parentCtrl">
<input ng-model="args">
<div ng-controller="childCtrl">
<input ng-model="args">
</div>
</div>
</body>
</html>
繼承作用域符合 JavaScript 的原型繼承機制,這意味著如果我們在子作用域中訪問一個父作用域中定義的屬性,JavaScript 首先在子作用域中尋找該屬性,沒找到再從原型鏈上的父作用域中尋找,如果還沒找到會再往上一級原型鏈的父作用域尋找。在 AngularJS 中,作用域原型鏈的頂端是$rootScope,AnguarJS 將會尋找到$rootScope 為止,如果還是找不到,則會返回 undefined。
我們用實例代碼說明下這個機制。首先,我們探討下對於原型數據類型的作用域繼承機制:
示例二:作用域繼承實例-原始類型數據繼承
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script type="text/javascript">
angular.module('app', [])
.controller('parentCtrl', ['$scope', function($scope) {
$scope.args = 'Nick DeveloperWorks';
}])
.controller('childCtrl', ['$scope', function($scope) {
}]);
</script>
<body ng-app="app">
<div ng-controller="parentCtrl">
<input ng-model="args">
<div ng-controller="childCtrl">
<input ng-model="args">
</div>
</div>
</body>
</html>
測試運行結果:
第一個輸入框:
雖然在 childCtrl 中沒有定義具體的 args 屬性,但是因為 childCtrl 的作用域繼承自 parentCtrl 的作用域,因此,AngularJS 會找到父作用域中的 args 屬性並設置到輸入框中。而且,如果我們在第一個輸入框中改變內容,內容將會同步的反應到第二個輸入框。
第二個輸入框:
第二個輸入框的內容從此將不再和第一個輸入框的內容保持同步。在改變第二個輸入框的內容時,因為 HTML 代碼中 model 明確綁定在 childCtrl 的作用域中,因此 AngularJS 會為 childCtrl 生成一個 args 原始類型屬性。這樣,根據 AngularJS 作用域繼承原型機制,childCtrl 在自己的作用域找得到 args 這個屬性,從而也不再會去尋找 parentCtrl 的 args 屬性。從此,兩個輸入框的內容所綁定的屬性已經是兩份不同的實例,因此不會再保持同步。
現將代碼做如下修改,結合以上兩個場景,會出現怎樣的結果?
示例三:作用域繼承實例-對象數據繼承
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script type="text/javascript">
angular.module('app', [])
.controller('parentCtrl', ['$scope', function($scope) {
$scope.args = {};
$scope.args.content = 'Nick DeveloperWorks';
}])
.controller('childCtrl', ['$scope', function($scope) {
}]);
</script>
<body ng-app="app">
<div ng-controller="parentCtrl">
<input ng-model="args.content">
<div ng-controller="childCtrl">
<input ng-model="args.content">
</div>
</div>
</body>
</html>
測試結果是無論改變任何一個輸入框的內容,兩者的內容始終同步。
根據 AngularJS 的原型繼承機制,如果 ng-model 綁定的是一個對象數據,那麼 AngularJS 將不會為 childCtrl 創建一個 args 的對象,自然也不會有 args.content 屬性。這樣,childCtrl 作用域中將始終不會存在 args.content 屬性,只能從父作用域中尋找,也即是兩個輸入框的的變化其實只是在改變 parentCtrl 作用域中的 args.content 屬性。因此,兩者的內容始終保持同步。
我們再看一個例子,分析結果如何。
示例四:作用域繼承實例-不再訪問父作用域的數據對象。
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script type="text/javascript">
angular.module('app', [])
.controller('parentCtrl', ['$scope', function($scope) {
$scope.args = {};
$scope.args.content = 'Nick DeveloperWorks';
}])
.controller('childCtrl', ['$scope', function($scope) {
$scope.args = {};
$scope.args.content = 'Nick DeveloperWorks for test';
}]);
</script>
<body ng-app="app">
<div ng-controller="parentCtrl">
<input ng-model="args.content">
<div ng-controller="childCtrl">
<input ng-model="args.content">
</div>
</div>
</body>
</html>
測試結果是兩個輸入框的內容永遠不會同步。子作用域有實例數據對象,則不訪問父作用域。
獨立作用域
獨立作用域是 AngularJS 中一個非常特殊的作用域,它只在 directive 中出現。在對 directive 的定義中,我們添加上一個 scope:{} 屬性,就為這個 directive 創建出了一個隔離作用域。
示例5: directive 創建出一個孤立作用域
angular.module('isolate', []).directive("isolate", function () {
return {
scope : {},
};
})
獨立作用域最大的特點是不會原型繼承其父作用域,對外界的父作用域保持相對的獨立。因此,如果在定義了孤立作用域的 AngularJS directive 中想要訪問其父作用域的屬性,則得到的值為 undefined。代碼如下:
示例六:獨立作用域的隔離性
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script type="text/javascript">
angular.module('app', [])
.controller('ctrl', ['$scope', function($scope) {
$scope.args = {};
}])
.directive("isolateDirective", function () {
return {
scope : {},
link : function($scope, $element, $attr) {
console.log($scope.$args); //輸出 undefined
}
};
});
</script>
<body ng-app="app">
<div ng-controller="ctrl">
<div isolate-directive></div>
</div>
</body>
</html>
上面的代碼中通過在 directive 中聲明瞭 scope 屬性從而創建了一個作用域,其父作用域為 ctrl 所屬的作用域。但是,這個作用域是孤立的,因此,它訪問不到父作用域的中的任何屬性。存在這樣設計機制的好處是:能夠創建出一些列可復用的 directive,這些 directive 不會相互在擁有的屬性值上產生串擾,也不會產生任何副作用。
AngularJS 獨立作用域的數據綁定
在繼承作用域中,我們可以選擇子作用域直接操作父作用域數據來實現父子作用域的通信,而在獨立作用域中,子作用域不能直接訪問和修改父作用域的屬性和值。為了能夠使孤立作用域也能和外界通信,AngularJS 提供了三種方式用來打破獨立作用域“孤立”這一限制。
單向綁定(@ 或者 @attr)
這是 AngularJS 獨立作用域與外界父作用域進行數據通信中最簡單的一種,綁定的對象只能是父作用域中的字元串值,並且為單向只讀引用,無法對父作用域中的字元串值進行修改,此外,這個字元串還必須在父作用域的 HTML 節點中以 attr(屬性)的方式聲明。
使用這種綁定方式時,需要在 directive 的 scope 屬性中明確指定引用父作用域中的 HTML 字元串屬性,否則會拋異常。示例代碼如下:
實例七: 單向綁定示例
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script>
angular.module('isolateScope', [])
.directive("isolateDirective", function () {
return {
replace : true,
template: '<button>{{isolates}}</button>',
scope : {
isolates : '@',
},
link : function($scope, $element, $attr) {
$scope.isolates = "DeveloperWorks";//無效
}
};
})
.controller("ctrl", function ($scope) {
$scope.btns = 'NICK';
});
</script>
<body ng-app="isolateScope" >
<div ng-controller="ctrl">
<button>{{btns}}</button>
<div isolate-directive isolates="{{btns}}"></div>
</div>
</body>
</html>
上面的代碼,通過在 directive 中聲明瞭 scope:{isolates:'@'} 使得 directive 擁有了父作用域中 data-isolates (isolates為自定義屬性,不加data也可以,但建議加上data)這個 HTML 屬性所擁有的值,這個值在控制器 ctrl 中被賦值為'nick'。所以,代碼的運行結果是頁面上有兩個名為 nick的按鈕。
我們還註意到 link 函數中對 isolates 進行了修改,但是最終不會在運行結果中體現。這是因為 isolates 始終綁定為父作用域中的 btns 字元串,如果父作用域中的 btns 不改變,那麼在孤立作用域中無論怎麼修改 isolates 都不會起作用。
引用綁定(&或者&attr)
通過這種形式的綁定,孤立作用域將有能力訪問到父作用域中的函數對象,從而能夠執行父作用域中的函數來獲取某些結果。這種方式的綁定跟單向綁定一樣,只能以只讀的方式訪問父作用函數,並且這個函數的定義必須寫在父作用域 HTML 中的 attr(屬性)節點上。
這種方式的綁定雖然無法修改父作用域的 attr 所設定的函數對象,但是卻可以通過執行函數來改變父作用域中某些屬性的值,來達到一些預期的效果。示例代碼如下:
示例八:引用綁定示例
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script>
angular.module('isolateScope', [])
.directive("isolateDirective", function () {
return {
replace : true,
scope : {
isolates : '&',
},
link : function($scope, $element, $attr) {
var func = $scope.isolates();
func();
}
};
})
.controller("ctrl", function ($scope) {
$scope.func = function () {
console.log("Nick DeveloperWorks");
}
});
</script>
<body ng-app="isolateScope" >
<div ng-controller="ctrl">
<div isolate-directive data-isolates="func"></div>
</div>
</body>
</html>
這個例子中,瀏覽器的控制台將會列印“Nick DeveloperWorks”文字。
上面的代碼中我們在父作用域中指定了一個函數對象$scope.func,在孤立作用域中通過對 HTML 屬性的綁定從而引用了 func。需要註意的是 link 函數中對 func 對象的使用方法,$scope.isolates 獲得的僅僅是函數對象,而不是調用這個對象,因此我們需要在調用完$scope.isolates 之後再調用這個函數,才能得到真正的執行結果。
雙向綁定(=或者=attr)
雙向綁定賦予 AngularJS 孤立作用域與外界最為自由的雙向數據通信功能。在雙向綁定模式下,孤立作用域能夠直接讀寫父作用域中的屬性和數據。和以上兩種孤立作用域定義數據綁定一樣,雙向綁定也必須在父作用域的 HTML 中設定屬性節點來綁定。
雙向綁定非常適用於一些子 directive 需要頻繁和父作用域進行數據交互,並且數據比較複雜的場景。不過,由於可以自由的讀寫父作用域中的屬性和對象,所以在一些多個 directive 共用父作用域數據的場景下需要小心使用,很容易引起數據上的混亂。
示例代碼如下:
示例九:雙向綁定示例
<!doctype html>
<html>
<head>
<meta charset=utf-8"/>
<title>scope nick</title>
<script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<script>
angular.module('isolateScope', [])
.directive("isolateDirective", function () {
return {
replace : true,
template: '<button>{{isolates}}</button>',
scope : {
isolates : '=',
},
link : function($scope, $element, $attr) {
$scope.isolates.name = "NICK";
}
};
})
.controller("ctrl", function ($scope) {
$scope.btns = {
name : 'nick',
dw : 'DeveloperWorks'
};
});
</script>
<body ng-app="isolateScope" >
<div ng-controller="ctrl">
<button>{{btns.dw}}</button>
<button>{{btns.name}}</button>
<div isolate-directive data-isolates="btns"></div>
</div>
</body>
</html>
上面的代碼運行的結果是瀏覽器頁面上出現三個按鈕,其中第一個按鈕標題為“DeveloperWorks”,第二和第三個按鈕的標題為“NICK”。
初始時父作用域中的$scope.btns.name為小寫的“nick”,通過雙向綁定,孤立作用域中將父作用域的 name改寫成為大寫的“NICK”並且直接生效,父作用域的值被更改。