本文執行個體講述了AngularJS的scope,繼承結構,事件系統和生命週期。分享給大家供大家參考,具體如下:
深入探討 Scope 範圍
每一個 $scope 都是類 Scope 的一個執行個體。類 Scope 擁有可以控制 scope 生命週期的方法,提供事件傳播的能力,並支援模板渲染。
範圍的階層
讓我們再來看看這個簡單的 HelloCtrl 的例子:
var HelloCtrl = function($scope){ $scope.name = 'World';}
HelloCtrl 看起來就跟普通的 JavaScript 建構函式沒什麼區別,事實上,除了 $scope 這個參數之外,確實沒什麼新奇之處。不過,這個參數究竟是從哪裡來的呢?
這個新的範圍是由 ng-controller指令使用 Scope.$new() 方法產生的。等一下,這麼說來我們必須至少擁有一個 scope 的執行個體才能建立新的 scope!沒錯,AngularJS其實有一個 $rootScope(這個是所有其他範圍的父級)。這個 $rootScope 執行個體是在一個新的應用啟動的時候建立的。
ng-controller指令就是 可以建立範圍 指令的其中一個。AngularJS 會在任何它在DOM樹中碰到這種 可以建立範圍 指令的時候建立一個新的 Scope類的執行個體。這些新建立的範圍通過 $parent 屬性指向它自身的父範圍。DOM樹中會有很多 可以建立範圍 的指令,結果就是,很多範圍被建立了。
範圍的形式類似於父子、樹狀的關係,並且最根部的就是 $rootScope 執行個體。就像範圍是被DOM樹驅動著建立的一樣,範圍樹也是在模仿 DOM 的結構。
現在你已經知道了,一些指令會建立新的子級的範圍,你可能會想,為什麼會需要這些複雜的東西。要想理解這一點,我們來示範一個例子,其中使用了 ng-repeat 迴圈指令。
控制器如下:
var WorldCtrl = function ($scope) { $scope.population = 7000; $scope.countries = [ {name: 'France', population: 63.1}, {name: 'United Kingdom', population: 61.8}, ];};
模版如下:
<ul ng-controller="WorldCtrl"> <li ng-repeat="country in countries"> {{country.name}} has population of {{country.population}} </li> <hr> World's population: {{population}} millions</ul>
這個 ng-repeat 指令可以迭代一個 countries 的集合,並且為集合中的每一項都建立新的DOM 元素。ng-repeat 指令的文法非常容易理解;其中每一項都需要一個新的變數 country,並把它掛到 $scope 上面,以便視圖渲染使用。
但這裡有一個問題,就是,每一個 country 都需要將一個新的變數掛載到一個 $scope 上去,而我們也不能就簡單的覆蓋掉前面被掛在上去的值。AngularJS 通過為集合中的每一個元素都建立一個新的範圍來解決這個問題。新建立的這些範圍跟相匹配的DOM樹結構非常相像,我們也能通過之前提到的那個牛逼的 Chrome 擴充 Batarang 來可視化的看到這一點。
每一個範圍(以矩形標註邊界)維護屬於她自己的一段資料模型。給不同的範圍增加同名的變數是完全沒有問題的,不會發生命名衝突(不同的DOM元素會指向不同的範圍,並使用相對應的範圍的變數來渲染模板)。這樣一來,每個元素又有自己的命名空間,在前面的例子中,每一個<li> 元素都有自己的範圍,而 country 變數就定義在各自的範圍上面。
Scope的階層和繼承
定義在作用於上的屬性對他的子級作用於來說是可見的,試想一下,子級範圍並不需要重複定義同名的屬性!這在實踐中是非常有用的,因為我們不必一遍又一遍的重複定義本來可以通過範圍鏈得到的那些屬性。
再來看看前面的例子,假設我們想要顯示給出的這些國家與世界總人口的百分比。要實現這個功能,我們可以在一個範圍上定義一個 worldsPercentage 的方法,並由 WorldCtrl 來管理,如下所以:
$scope.worldsPercentage = function (countryPopulation) { return (countryPopulation / $scope.population)*100;}
然後被 ng-repeat 建立的每一個範圍執行個體都來調用這個方法,如下:
<li ng-repeat="country in countries"> {{country.name}} has population of {{country.population}}, {{worldsPercentage(country.population)}} % of the World's population</li>
AngularJS中範圍的繼承規則跟 JavaScript 中原型的繼承規則是相同的(在需要讀取一個屬性的時候,會一直向繼承樹的上方查詢,直到找到了這個屬性為止)。
貫穿範圍鏈的繼承的風險
這種透過範圍層次關係的繼承,在讀資料的時候顯得非常的直觀、易於理解。但是在寫資料的時候,就變的有點複雜了。
讓我們來看看,如果我們在一個範圍上定義了一個變數,先不管是否在子級範圍上。JavaScript代碼如下:
var HelloCtrl = function ($scope) {};
視圖的代碼如下:
<body ng-app ng-init="name='World'"> <h1>Hello, {{name}}</h1> <div ng-controller="HelloCtrl"> Say hello to: <input type="text" ng-model="name"> <h2>Hello, {{name}}!</h2> </div></body>
運行一下這段代碼,就可以發現,這個 name 變數儘管僅僅是定義在了最頂級的範圍上,但在整個應用中都是可見的!這說明變數是從範圍鏈上繼承下來的。換句話說,變數是在父級範圍上定義的,然後在子級範圍中訪問的。
現在,我們一起來看看,如果在 <input> 中寫點字會發生什麼,運行結果你可能會感到吃驚,因為 HelloCtrl 控制器所初始化的範圍建立了一個新的變數,而不是直接去修改$rootScope 執行個體中的值。不過當我們認識到範圍也只不過是在彼此間進行了原型繼承,也就不會覺得那麼吃驚了。所有可以用在 JavaScript 對象上的原型繼承的規則,都可以同等的用在 範圍 的原型鏈繼承上去。畢竟 Scopes 範圍就是 JavaScript 對象嘛。
在子級範圍中去改變父級範圍上面的屬性有幾種方法。第一種,我們就直接通過 $parent 屬性來引用父級範圍,但我們要看到,這是一個非常不可靠的解決方案。麻煩之處就在於,ng-model 指令所使用的運算式非常嚴重的依賴於整個DOM結構。比如就在 <input> 標籤上面的哪裡插入另一個 可建立範圍 的指令,那$parent 就會指向一個完全不同的範圍了。
就經驗來講,盡量避免使用 $parent 屬性,因為它強制的把 AngularJS 運算式和你的模板所建立的 DOM 結構捆綁在了一起。這樣一來,HTML結構的一個小小的改動,都可能會讓整個應用崩潰。
另一個解決方案就是,不要直接把屬性綁定到 範圍上,而是綁到一個對象上面,如下所示:
<body ng-app ng-init="thing = {name : 'World'}"> <h1>Hello, {{thing.name}}</h1> <div ng-controller="HelloCtrl"> Say hello to: <input type="text" ng-model="thing.name"> <h2>Hello, {{thing.name}}!</h2> </div></body>
這個方案會更可靠,因為他並沒有假設 DOM 樹的結構是什麼樣子。
避免直接把資料繫結到 範圍的屬性上。應優先選擇把資料雙向繫結到對象的屬性上(然後再把對象掛到 scope 上)。就經驗而言,在給 ng-model 指令的運算式中,你應該有一個點(例如, ng-model="thing.name")。
範圍層級和事件系統
層級關係中的範圍可以使用 event bus(一種事件系統)。AngularJS可以在範圍層級中傳播具名的裝備齊全的事件。事件可以從任何一個範圍中發出,然後向上($emit)和向下($broadcast)四處傳播。
AngularJS核心服務和指令使用這種事件巴士來發出一些應用程式狀態變化的重要事件。比如,我們可以監聽$locationChangeSuccess 事件(由 $rootScope 執行個體發出),然後在任何 location(瀏覽器中就是URL)變化的時候都會得到通知,如下所示:
$scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl){ //react on the location change here //for example, update breadcrumbs based on the newUrl});
每一個範圍對象都會有這個 $on 方法,可以用來註冊一個範圍事件的接聽程式。這個函數所扮演的接聽程式在被調用時會有一個 event 對象作為第一個參數。後面的參數會根據事件類型的不同與事件本身的配備一一對應。
類似於 DOM 事件,我們可以調用 event 對象的 preventDefault() 和 stopPropagation() 方法。stopPropagation() 方法將會阻止事件沿著範圍層級繼續冒泡,並且只在事件向上層傳播的時候($emit)才有效。
儘管 AngularJS 的事件系統是模仿了 DOM 的,但兩個事件傳播系統是完全獨立的,沒有任何共同之處。
雖然在範圍層級中傳播事件對一些問題來說是一種非常優雅方案(特別是對全域的,非同步狀態變化來說),但還是要適度使用。通常情況下,可以依靠雙向資料繫結來得到一個比較乾淨的方案。在整個 AngularJS 架構中,一共只發出($emit)了三個事件($includeContentRequested,$includeContentLoaded,$viewContentLoaded)和七個廣播($broadcast)($locationChangeStart, $locationChangeSuccess, $routeUpdate, $routeChangeStart,$routeChangeSuccess, $routeChangeError, $destroy)。正如你所看到的,範圍事件使用的非常少,我們應該在發送自訂的事件之前認真的評估一下其他的可選方案(多數會是雙向資料繫結)。
千萬不要在 AngularJS 中模仿 DOM 的基於事件的編程方式。大多數情況下,你的應用會有更好的架構方式,你也可以在雙向資料繫結這條路上深入探索。
範圍的生命週期
範圍需要提供相互隔離的命名空間,避免變數的命名衝突。範圍們都很小,而且被以層級的方式組織起來,對記憶體使用量的管理來說很有協助。當其中一個範圍不再需要 ,它就可以被銷毀了。結果就是,這個範圍所暴露出來的模型和方法就符合的記憶體回收的標準。
新的範圍通常是被 可建立範圍 的指令所產生和銷毀的。不過也可以使用 $new() 和 $destroy() 方法來手動的建立和銷毀範圍。
希望本文所述對大家AngularJS程式設計有所協助。