詳解JavaScript的AngularJS架構中的範圍與資料繫結_AngularJS

來源:互聯網
上載者:User

AngularJS 簡介
AngularJS 是由 Google 發起的一款開源的前端 MVC 指令碼架構,既適合做普通 WEB 應用也可以做 SPA(單頁面應用,所有的使用者操作都在一個頁面中完成)。與同為 MVC 架構的 Dojo 的定位不同,AngularJS 在功能上更加輕量,而相比於 jQuery,AngularJS 又幫您省去了許多機械的綁定工作。在一些對開發速度要求高,功能模組不需要太豐富的非企業級 WEB 應用上,AngularJS 是一個非常好的選擇。AngularJS 最為複雜同時也是最強大的部分就是它的資料繫結機制,這個機制協助我們能更好的將注意力集中在資料的模型建立和傳遞上,而不是對底層的 DOM 進行低級的操作。

AngularJS 範圍
基於 jQuery 的傳統 WEB 應用中,為了監聽使用者的輸入等行為,需要為每一個 DOM 元素設定一個監聽方法,也即是監聽 DOM 上發生的各類事件,然後由 jQuery 做出回應並展示在頁面上。這種方法簡便直觀,但是一旦 WEB 應用變得龐大而且複雜,那麼監聽代碼就顯得非常的機械而且冗餘,更可怕的是,如果對於 DOM 的事件監聽沒有做好管理,那麼很容易出現瀏覽器資源的泄露。
針對以上所暴露的問題,AngularJS 用一系列指令來代替 jQuery 的事件綁定代碼。為了能夠組織好各類指令之間的協調工作而不出現資料混亂,AngularJS 在模型層上引申出範圍的概念,以配合控制器來實現對視圖層的展現工作。
範圍(Scope)
AngularJS 中,範圍是一個指嚮應用模型的對象,它是運算式的執行環境。範圍有階層,這個層次和相應的 DOM 幾乎是一樣的。範圍能監控運算式和傳遞事件。
在 HTML 程式碼中,一旦一個 ng-app 指令被定義,那麼一個範圍就產生了,由 ng-app 所產生的範圍比較特殊,它是一個根範圍($rootScope),它是其他所有$Scope 的最頂層。
清單 1. 產生根範圍

<html> <head><script src="angular.min.js"></script></head> <body data-ng-app="app">...</body></html>

除了用 ng-app 指令可以產生一個範圍之外,其他的指令如 ng-controller,ng-repeat 等都會產生一個或者多個範圍。此外,還可以通過 AngularJS 提供的建立範圍的Factory 方法來建立一個範圍。這些範圍都擁有自己的繼承上下文,並且根範圍都為$rootScope。
在產生一個範圍之後,在編寫 AngularJS 代碼時,$scope 對象就代表了這個範圍的資料實體,我們可以在$scope 內定義各種資料類型,之後可以直接在 HTML 中以 {{變數名}} 方式來讓 HTML 訪問到這個變數,代碼如下:
清單 2. 簡單的資料繫結

<script>angular.module('app', []) .controller("ctrl", function ($scope) { $scope.btns = { ibm : 'ibm' }; });</script></head><body data-ng-app="app" > <div data-ng-controller="ctrl"> <button>{{btns.ibm}}</button> </div></body>

這就是 AngularJS 中最簡單的資料繫結方式,同時也是應用最為廣泛的資料繫結方式。
繼承範圍(Inherited Scope)
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 的子範圍。
清單 3. 範圍的繼承執行個體

<body data-ng-app="app"> <div data-ng-controller="parentCtrl"><input data-ng-model="args"><div data-ng-controller="childCtrl"> <input data-ng-model="args"></div> </div></body>

繼承範圍符合 JavaScript 的原型繼承機制,這意味著如果我們在子範圍中訪問一個父範圍中定義的屬性,JavaScript 首先在子範圍中尋找該屬性,沒找到再從原型鏈上的父範圍中尋找,如果還沒找到會再往上一級原型鏈的父範圍尋找。在 AngularJS 中,範圍原型鏈的頂端是$rootScope,AnguarJS 將會尋找到$rootScope 為止,如果還是找不到,則會返回 undefined。
我們用執行個體代碼說明下這個機制。首先,我們探討下對於原型資料類型的範圍繼承機制:
清單 4. 範圍繼承執行個體-原始類型資料繼承

<script type="text/javascript"> angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = 'IBM DeveloperWorks'; }]) .controller('childCtrl', ['$scope', function($scope) {  }]);</script><body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args"><div data-ng-controller="childCtrl"> <input data-ng-model="args"></div> </div></body>

運行頁面,我們得到以下的結果:
圖 1. 頁面運行結果。

這個結果我們非常好理解,雖然在 childCtrl 中沒有定義具體的 args 屬性,但是因為 childCtrl 的範圍繼承自 parentCtrl 的範圍,因此,AngularJS 會找到父範圍中的 args 屬性並設定到輸入框中。而且,如果我們在第一個輸入框中改變內容,內容將會同步的反應到第二個輸入框:
圖 2. 改變第一個輸入框的內容後頁面運行結果

假如我們修改第二個輸入框的內容,此時會發生什麼事情呢?答案是第二個輸入框的內容從此將不再和第一個輸入框的內容保持同步。在改變第二個輸入框的內容時,因為 HTML 程式碼中 model 明確綁定在 childCtrl 的範圍中,因此 AngularJS 會為 childCtrl 產生一個 args 原始類型屬性。這樣,根據 AngularJS 範圍繼承原型機制,childCtrl 在自己的範圍找得到 args 這個屬性,從而也不再會去尋找 parentCtrl 的 args 屬性。從此,兩個輸入框的內容所綁定的屬性已經是兩份不同的執行個體,因此不會再保持同步。
圖 3. 改變第二個輸入框的內容後頁面運行結果

假如我們將代碼做如下修改,結合以上兩個情境,思考下會出現怎樣的結果?
清單 5. 範圍繼承執行個體-對象資料繼承

<script type="text/javascript">angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = {}; $scope.args.content = 'IBM DeveloperWorks';}]).controller('childCtrl', ['$scope', function($scope) { }]);</script><body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args.content"> <div data-ng-controller="childCtrl"> <input data-ng-model="args.content"> </div> </div></body>

答案是無論改變任何一個輸入框的內容,兩者的內容始終同步。
根據 AngularJS 的原型繼承機制,如果 ng-model 綁定的是一個對象資料,那麼 AngularJS 將不會為 childCtrl 建立一個 args 的對象,自然也不會有 args.content 屬性。這樣,childCtrl 範圍中將始終不會存在 args.content 屬性,只能從父範圍中尋找,也即是兩個輸入框的的變化其實只是在改變 parentCtrl 範圍中的 args.content 屬性。因此,兩者的內容始終保持同步。
我們再看一個例子,這次請讀者自行分析結果。
清單 6. 範圍繼承執行個體-不再訪問父範圍的資料對象。

<script type="text/javascript"> angular.module('app', []) .controller('parentCtrl', ['$scope', function($scope) { $scope.args = {}; $scope.args.content = 'IBM DeveloperWorks';}]).controller('childCtrl', ['$scope', function($scope) { $scope.args = {};  $scope.args.content = 'IBM DeveloperWorks';}]);</script><body data-ng-app="app"> <div data-ng-controller="parentCtrl"> <input data-ng-model="args.content"> <div data-ng-controller="childCtrl"> <input data-ng-model="args.content"> </div> </div></body>

答案是兩個輸入框的內容永遠不會同步。
孤立範圍(Isolate Scope)
孤立範圍是 AngularJS 中一個非常特殊的範圍,它只在 directive 中出現。在對 directive 的定義中,我們添加上一個 scope:{} 屬性,就為這個 directive 建立出了一個隔離範圍。
清單 7. directive 建立出一個孤立範圍

angular.module('isolate', []).directive("isolate", function () { return { scope : {}, };})

孤立範圍最大的特點是不會原型繼承其父範圍,對外界的父範圍保持相對的獨立。因此,如果在定義了孤立範圍的 AngularJS directive 中想要訪問其父範圍的屬性,則得到的值為 undefined。代碼如下:
清單 8. 孤立範圍的隔離性

<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 data-ng-app="app"> <div data-ng-controller="ctrl"> <div data-isolate-directive></div> </div></body>

上面的代碼中通過在 directive 中聲明了 scope 屬性從而建立了一個範圍,其父範圍為 ctrl 所屬的範圍。但是,這個範圍是孤立的,因此,它訪問不到父範圍的中的任何屬性。存在這樣設計機制的好處是:能夠建立出一些列可複用的 directive,這些 directive 不會相互在擁有的屬性值上產生串擾,也不會產生任何副作用。
AngularJS 孤立範圍的資料繫結
在繼承範圍中,我們可以選擇子範圍直接操作父範圍資料來實現父子範圍的通訊,而在孤立範圍中,子範圍不能直接存取和修改父範圍的屬性和值。為了能夠使孤立範圍也能和外界通訊,AngularJS 提供了三種方式用來打破孤立範圍“孤立”這一限制。
單向綁定(@ 或者 @attr)
這是 AngularJS 孤立範圍與外界父範圍進行資料通訊中最簡單的一種,綁定的對象只能是父範圍中的字串值,並且為單向唯讀引用,無法對父範圍中的字串值進行修改,此外,這個字串還必須在父範圍的 HTML 節點中以 attr(屬性)的方式聲明。
使用這種綁定方式時,需要在 directive 的 scope 屬性中明確指定引用父範圍中的 HTML 字串屬性,否則會拋異常。範例程式碼如下:
清單 9. 單向綁定樣本

<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 = 'IBM'; });</script><body data-ng-app="isolateScope" ><div data-ng-controller="ctrl"> <button>{{btns}}</button> <div data-isolate-directive data-isolates="{{btns}}"></div> </div></body>

簡單分析下上面的代碼,通過在 directive 中聲明了 scope:{isolates:'@'} 使得 directive 擁有了父範圍中 data-isolates 這個 HTML 屬性所擁有的值,這個值在控制器 ctrl 中被賦值為'IBM'。所以,代碼的運行結果是頁面上有兩個名為 IBM 的按鈕。
我們還注意到 link 函數中對 isolates 進行了修改,但是最終不會在運行結果中體現。這是因為 isolates 始終綁定為父範圍中的 btns 字串,如果父範圍中的 btns 不改變,那麼在孤立範圍中無論怎麼修改 isolates 都不會起作用。
引用綁定(&或者&attr)
通過這種形式的綁定,孤立範圍將有能力訪問到父範圍中的函數對象,從而能夠執行父範圍中的函數來擷取某些結果。這種方式的綁定跟單向綁定一樣,只能以唯讀方式訪問父作用函數,並且這個函數的定義必須寫在父範圍 HTML 中的 attr(屬性)節點上。
這種方式的綁定雖然無法修改父範圍的 attr 所設定的函數對象,但是卻可以通過執行函數來改變父範圍中某些屬性的值,來達到一些預期的效果。範例程式碼如下:
清單 10. 引用綁定樣本

<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("IBM DeveloperWorks"); } });</script><body data-ng-app="isolateScope" > <div data-ng-controller="ctrl">  <div data-isolate-directive data-isolates="func"></div> </div> </body>

這個例子中,瀏覽器的控制台將會輸出一段“IBM DeveloperWorks”文字。
上面的代碼中我們在父範圍中指定了一個函數對象$scope.func,在孤立範圍中通過對 HTML 屬性的綁定從而引用了 func。需要注意的是 link 函數中對 func 對象的使用方法,$scope.isolates 獲得的僅僅是函數對象,而不是調用這個對象,因此我們需要在調用完$scope.isolates 之後再調用這個函數,才能得到真正的執行結果。
雙向繫結(=或者=attr)
雙向繫結賦予 AngularJS 孤立範圍與外界最為自由的雙向資料通訊功能。在雙向繫結模式下,孤立範圍能夠直接讀寫父範圍中的屬性和資料。和以上兩種孤立範圍定義資料繫結一樣,雙向繫結也必須在父範圍的 HTML 中設定屬性節點來綁定。
雙向繫結非常適用於一些子 directive 需要頻繁和父範圍進行資料互動,並且資料比較複雜的情境。不過,由於可以自由的讀寫父範圍中的屬性和對象,所以在一些多個 directive 共用父範圍資料的情境下需要小心使用,很容易引起資料上的混亂。
範例程式碼如下:
清單 11. 雙向繫結樣本

<script> angular.module('isolateScope', []) .directive("isolateDirective", function () { return { replace : true, template: '<button>{{isolates}}</button>', scope : { isolates : '=', }, link : function($scope, $element, $attr) { $scope.isolates.ibm = "IBM"; } }; }) .controller("ctrl", function ($scope) { $scope.btns = { ibm : 'ibm', dw : 'DeveloperWorks' };  });</script><body data-ng-app="isolateScope" > <div data-ng-controller="ctrl"> <button>{{btns.dw}}</button> <button>{{btns.ibm}}</button> <div data-isolate-directive data-isolates="btns"></div> </div></body>

上面的代碼啟動並執行結果是瀏覽器頁面上出現三個按鈕,其中第一個按鈕標題為“DeveloperWorks”,第二和第三個按鈕的標題為“IBM”。
初始時父範圍中的$scope.btns.ibm 為小寫“ibm”,通過雙向繫結,孤立範圍中將父範圍的 ibm 改寫成為大寫的“IBM”並且直接生效,父範圍的值被更改。

總結
由於 AngularJS 架構的輕量性和其清晰的 MVC 特點使得其在推出之後就大受歡迎,實踐中也很容易上手。AngularJS 比較難以掌握和理解的就是其範圍和綁定機制,本文重點將範圍和綁定機製做了分析與討論,希望讀者能夠理解並熟練掌握這塊內容。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.