Angular之做用域與事件(轉)

學習Angular,首先要理解其做用域機制。html

Angular應用是分層的,主要有三個層面:視圖,模型,視圖模型。其中,視圖很好理解,就是直接可見的界面,模型就是數據,那麼視圖模型是什麼呢?是一種把數據包裝給視圖調用的東西。git

所謂做用域,也就是視圖模型中的一個概念。github

根做用域

在第一章中,有這麼一個很簡單的數據綁定例子:數組

<input ng-model="rootA"/>
<div>{{rootA}}</div>

 

當時咱們解釋過,這個例子可以運行的的緣由是,它的rootA變量被建立在根做用域上。每一個Angular應用默認有一個根做用域,也就是說,若是用戶未指定本身的控制器,變量就是直接掛在這個層級上的。瀏覽器

做用域在一個Angular應用中是以樹的形狀體現的,根做用域位於最頂層,從它往下掛着各級做用域。每一級做用域上面掛着變量和方法,供所屬的視圖調用。架構

若是想要在代碼中顯式使用根做用域,能夠注入$rootScope。app

怎麼證明剛纔的例子中,$rootScope確實存在,並且變量真的在它上面呢?咱們來寫個代碼:ide

function RootService($rootScope) {
    $rootScope.$watch("rootA", function(newVal) {
        alert(newVal);
    });
}

 

這時候咱們能夠看到,這段代碼並未跟界面產生任何關係,但裏面的監控表達式確實生效了,也就是說,觀測到了根做用域上rootA的變動,說明有人給它賦值了。函數

做用域的繼承關係

在開發過程當中,咱們可能會出現控制器的嵌套,看下面這段代碼:學習

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
    </div>
</div>
function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

注意結果,咱們能夠看到界面顯示了兩個1,而咱們只在OuterCtrl的做用域裏定義了a變量,但界面給咱們的結果是,兩個a都有值。這裏內層的a值顯然來自外層,由於當咱們對界面做出這樣的調整以後,就只有一個了:

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
</div>

<div ng-controller="InnerCtrl">
    <span>{{a}}</span>
</div>

 

這是爲何呢?在Angular中,若是兩個控制器所對應的視圖存在上下級關係,它們的做用域就自動產生繼承關係。什麼意思呢?

先考慮在純JavaScript代碼中,兩個構造函數各自有一個實例:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
var inner = new Inner();

 

在這裏面添加什麼代碼,可以讓inner.a == 1呢?

熟悉JavaScript原型的咱們,固然絕不猶豫就加了一句:Inner.prototype = outer;

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

 

因而就獲得想要的結果了。

再回到咱們的例子裏,Angular的實現機制其實也就是把這兩個控制器中的$scope做了關聯,外層的做用域實例成爲了內層做用域的原型。

以此類推,整個Angular應用的做用域,都存在自頂向下的繼承關係,最頂層的是$rootScope,而後一級一級,沿着不一樣的控制器往下,造成了一棵做用域的樹,這也就像封建社會:天子高高在上,分茅裂土,公侯伯子男,一級一級往下,層層從屬。

簡單變量的取值與賦值

既然做用域是經過原型來繼承的,天然也就能夠推論出一些特徵來。好比說這段代碼,點擊按鈕的結果是什麼?

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="a=a+1">a++</button>
    </div>
</div>

 

function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

點了按鈕以後,兩個a不一致了,裏面的變了,外面的沒變,這是爲何?原先兩層不是共用一個a嗎,怎麼會出現兩個不一樣的值?看這句就能明白了,至關於咱們以前那個例子裏,這樣賦值了:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

inner.a = inner.a + 1;

 

最後這句,頗有意思,它有兩個過程,取值的時候,由於inner自身上面沒有,因此沿着原型往上取到了1,而後自增了以後,賦值給本身,這個賦值的時候就不一樣了,敬愛的林副主席教導咱們:有a就賦值,沒有a,創造一個a也要賦值。

因此這麼一來,inner上面就被賦值了一個新的a,outer裏面的仍然保持原樣,這也就致使了剛纔看到的結果。

初學者在這個問題上很容易犯錯,若是不能隨時很明確地認識到這些變量的差別,很容易寫出有問題的程序。既然這樣,咱們能夠用一些別的方式來減小變量的歧義。

對象在上下級做用域之間的共享

好比說,咱們就是想上下級共享變量,不建立新的,該怎麼辦呢?

考慮下面這個例子:

function Outer() {
    this.data = {
        a: 1
    };
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;

var inner = new Inner();

console.log(outer.data.a);
console.log(inner.data.a);

// 注意,這個時候會怎樣?
inner.data.a += 1;

console.log(outer.data.a);
console.log(inner.data.a);

 

此次的結果就跟上次不一樣了,緣由是什麼呢?由於二者的data是同一個引用,對這個對象上面的屬性修改,是能夠反映到兩級對象上的。咱們經過引入一個data對象的方式,繼續使用了原先的變量。把這個代碼移植到AngularJS裏,就變成了下面這樣:

<div ng-controller="OuterCtrl">
    <span>{{data.a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{data.a}}</span>
        <button ng-click="data.a=data.a+1">increase a</button>
    </div>
</div>
function OuterCtrl($scope) {
    $scope.data = {
        a: 1
    };
}

function InnerCtrl($scope) {
}

 

從這個例子咱們就發現了,若是想要避免變量歧義,顯式指定所要使用的變量會是比較好的方式,那麼若是咱們確實就是要在上下級分別存在相同的變量該怎麼辦呢,好比說下級的點擊,想要給上級的a增長1,咱們可使用$parent來指定上級做用域。

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="$parent.a=a+1">increase a</button>
    </div>
</div>

 

function OuterCtrl($scope) {
    $scope.a = 1;
}

function InnerCtrl($scope) {
}

 

控制器實例別名

從Angular 1.2開始,引入了控制器實例的別名機制。在以前,可能都須要向控制器注入$scope,而後,控制器裏面定義可綁定屬性和方法都是這樣:

function CtrlA($scope) {
    $scope.a = 1;
    $scope.foo = function() {
    };
}

 

<div ng-controller="CtrlA">
    <div>{{a}}</div>
    <button ng-click="foo()">click me</button>
</div>

 

其實$scope的注入是一個比較冗餘的概念,沒有必要把這種概念過度暴露給用戶。在應用中出現的做用域,有的是充當視圖模型,而有些則是處於隔離數據的須要,前者如ng-controller,後者如ng-repeat。在最近版本的AngularJS中,已經能夠不顯式注入$scope了,語法是這樣:

function CtrlB() {
    this.a = 1;
    this.foo = function() {
    };
}

 

這裏面,就徹底沒有$scope的身影了,那這個控制器怎麼使用呢?

<div ng-controller="CtrlB as instanceB">
    <div>{{instanceB.a}}</div>
    <button ng-click="instanceB.foo()">click me</button>
</div>

 

注意咱們在引入控制器的時候,加了一個as語法,給CtrlB的實例取了一個別名叫作instanceB,這樣,它下屬的各級視圖均可以顯式使用這個名稱來調用其屬性和方法,不易引發歧義。

在開發過程當中,爲了不模板中的變量歧義,應當儘量使用命名限定,好比a.b,出現歧義的可能性就比單獨的b要少得多。

不請自來的新做用域

在一個應用中,最多見的會建立做用域的指令是ng-controller,這個很好理解,由於它會實例化一個新的控制器,往裏面注入一個$scope,也就是一個新的做用域,因此通常人都會很天然地理解這裏面的做用域隔離關係。可是對於另一些狀況,就有些困惑了,好比說,ng-repeat,怎麼理解這個東西也會建立新做用域呢?

仍是看以前的例子:

$scope.arr = [1, 2, 3];

  

<ul>
    <li ng-repeat="item in arr track by $index">{{item}}</li>
</ul>

 

在ng-repeat的表達式裏,有一個item,咱們來思考一下,這個item是個什麼狀況。在這裏,數組中有三個元素,在循環的時候,這三個元素都叫作item,這時候就有個問題,如何區分每一個不一樣的item,可能咱們這個例子還不夠直接,那改一下:

<div>outer: {{sum1}}</div>
<ul>
    <li ng-repeat="item in arr track by $index">
        {{item}}
        <button ng-click="sum1=sum1+item">increase</button>
        <div>inner: {{sum1}}</div>
    </li>
</ul>

 

這個例子運行一下,咱們會發現每一個item都會獨立改變,說明它們確實是區分開了的。事實上,Angular在這裏爲ng-repeat的每一個子項都建立了單獨的做用域,因此,每一個item都存在於本身的做用域裏,互不影響。有時候,咱們是須要在循環內部訪問外層變量的,回憶一下,在本章的前面部分中,咱們舉例說,若是兩個控制器,它們的視圖有包含關係,內層控制器的做用域能夠經過$parent來訪問外層控制器做用域上的變量,那麼,在這種循環裏,是否是也能夠如此呢?

看這個例子:

<div>outer: {{sum2}}</div>
<ul>
    <li ng-repeat="item in arr track by $index">
        {{item}}
        <button ng-click="$parent.sum2=sum2+item">increase</button>
        <div>inner: {{sum2}}</div>
    </li>
</ul>

 

果真是能夠的。不少時候,人們會把$parent誤認爲是上下兩級控制器之間的訪問通道,但從這個例子咱們能夠看到,並不是如此,只是兩級做用域而已,做用域跟控制器仍是不一樣的,剛纔的循環能夠說是有兩級做用域,但都處於同一個控制器之中。

剛纔咱們已經提到了ng-controller和ng-repeat這兩個經常使用的內置指令,二者都會建立新的做用域,除此以外,還有一些其餘指令也會建立新的做用域,不少初學者在使用過程當中很容易產生困擾。

第一章咱們提到用ng-show和ng-hide來控制某個界面塊的總體展現和隱藏,但一樣的功能其實也能夠用ng-if來實現。那麼這二者的差別是什麼呢,所謂show和hide,你們很好理解,就是某個東西原先有,只是控制是否顯式,而if的含義是,若是知足條件,就建立這塊DOM,不然不建立。因此,ng-if所控制的界面塊,只有條件爲真的時候纔會存在於DOM樹中。

除此以外,二者還有個差別,ng-show和ng-hide是不自帶做用域的,而ng-if則本身建立了一級做用域。在用的時候,二者就是有差異的,好比說內部元素訪問外層定義的變量,就須要使用相似ng-repeat那樣的$parent語法了。

類似的類型還有ng-switch,ng-include等等,規律能夠總結,也就是那些會動態建立一塊界面的東西,都是自帶一級做用域。

「懸空」的做用域

通常而言,在Angular工程中,基本是不須要手動建立做用域的,但真想建立的話,也是能夠作到的。在任意一個已有的做用域上調用$new(),就能建立一個新的做用域:

var newScope = scope.$new();

 

剛建立出來的做用域是一個「懸空」的做用域,也就是說,它跟任何界面模板都不存在綁定關係,建立它的做用域會成爲它的$parent。這種做用域能夠通過$compile階段,與某視圖模板進行融合。

爲了幫助理解,咱們能夠用DocumentFragment做類比,看成用域被建立的時候,就比如是建立了一個DocumentFragment,它是不在DOM樹上的,只有當它被append到DOM樹上,纔可以被當作普通的DOM來使用。

那麼,懸空的做用域是否是什麼用處都沒有呢?也不是,儘管它未與視圖關聯,可是它的一些方法仍然能夠用。

咱們在第一章裏提到了$watch,這就是定義在做用域原型上的。若是咱們想要監控一個數據的變化,但這個數據並不是綁定到界面上的,好比下面這樣,怎麼辦?

function IsolateCtrl($scope) {
    var child = {
        a: 1
    };

    child.a++;
}

 

注意這個child,它並未綁定到$scope上,若是咱們想要在a變化的時候作某些事情,是沒有辦法作的,由於直到最近的某些瀏覽器中,才實現了Object.observe這樣的對象變動觀測方法,以前某些瀏覽器中要作這些,會比較麻煩。

可是咱們的$watch和$eval之類的方法,其實都是實如今做用域對象上的,也就是說,任何一個做用域,即便沒有與界面產生關聯,也是可以使用這些方法的。

function IsolateCtrl($scope) {
    var child = $scope.$new();
    child.a = 1;

    child.$watch("a", function(newValue) {
        alert(newValue);
    });

    $scope.change = function() {
        child.a++;
    };
}

 

這時候child裏面a的變動就能夠被觀測到,而且,這個child只有本做用域能夠訪問到,至關因而一個加強版的數據模型。若是咱們要作一個小型流程引擎之類的東西,做用域對象上提供的這些方法會頗有用。

做用域上的事件

咱們剛纔提到使用$parent來處理上下級的通信,但其實這不是一種好的方式,尤爲是在不一樣控制器之間,這會增長它們的耦合,對組件複用很不利。那怎樣才能更好地解耦呢?咱們可使用事件。

提到事件,可能不少人想到的都是DOM事件,其實DOM事件只存在於上層,並且沒有業務含義,若是咱們想要傳遞一個明確的業務消息,就須要使用業務事件。這種所謂的業務事件,其實就是一種消息的傳遞。

假設有如圖所示的應用:

事件的傳遞

這張圖中有一個應用,下面存在兩個視圖塊A和B,它們分別又有兩個子視圖。這時候,若是子視圖A1想要發出一個業務事件,使得B1和B2可以獲得通知,過程就會是:

  • 沿着父做用域一路往上到達雙方共同的祖先做用域
  • 從祖先做用域一級一級往下進行廣播,直到到達須要的地方

剛纔的圖形體現了界面的包含關係,若是把這個圖再立體化,就會是下面這樣:

事件的傳遞

對於這種事件的傳播方式,能夠有個相似的比喻:

好比說,某軍隊中,1營1連1排長想要給1營2連下屬的三個排發個警惕通知,他的通知方向是一級一級向上彙報,直到雙方共同的上級,也就是1營指揮人員這裏,而後再沿着二連這個路線向下去通知。

  • 從做用域往上發送事件,使用scope.$emit
$scope.$emit("someEvent", {});

 

  • 從做用域往下發送事件,使用scope.$broadcast
$scope.$broadcast("someEvent", {});

 

這兩個方法的第二個參數是要隨事件帶出的數據。

注意,這兩種方式傳播事件,事件的發送方本身也會收到一份。

使用事件的主要做用是消除模塊間的耦合,發送方是不須要知道接收方的情況的,接收方也不須要知道發送方的情況,雙方只須要傳送必要的業務數據便可。

事件的接收與阻止

不管是$emit仍是$broadcast發送的事件,均可以被接收,接收這兩種事件的方式是同樣的:

$scope.$on("someEvent", function(e) {
    // 這裏從e上能夠取到發送過來的數據
});

 

注意,事件被接收了,並不表明它就停止了,它仍然會沿着原來的方向繼續傳播,也就是:

  • $emit的事件將繼續向上傳播
  • $broadcast的事件將繼續向下傳播

有時候,咱們但願某一級收到事件以後,就讓它停下來,再也不傳播,能夠把事件停止。這時候,兩種事件的區別就體現出來了,只有$emit發出的事件是能夠被停止的,$broadcast發出的不能夠。

若是想要阻止$emit事件的繼續傳播,能夠調用事件對象的stopPropagation()方法。

$scope.$on("someEvent", function(e) {
    e.stopPropagation();
});

 

可是,想要阻止$broadcast事件的傳播,就麻煩了,咱們只能經過變通的方式:

首先,調用事件對象的preventDefault()方法,而後,在收取這個事件對象的時候,判斷它的defaultPrevented屬性,若是爲true,就忽略此事件。這個過程比較麻煩,其實咱們通常是不須要管的,只要不監聽對應的事件就能夠了。在實際使用過程當中,也應當儘可能少使用事件的廣播,尤爲是從較高的層級進行廣播。

上級做用域

$scope.$on("someEvent", function(e) {
    e.preventDefault();
});

 

下級做用域

$scope.$on("someEvent", function(e) {
    if (e.defaultPrevented) {
        return;
    }
});

 

事件總線

在Angular中,不一樣層級做用域之間的數據通訊有多種方式,能夠經過原型繼承的一些特徵,也能夠收發事件,還可使用服務來構造單例對象進行通訊。

前面提到的這個軍隊的例子,有些時候溝通效率比較低,特別是層級多的時候。想象一下,剛纔這個只有三層,若是更復雜,一個排長的消息都必定要報告到軍長那邊再下發到其餘基層主官,一定貽誤軍情,更況且有不少下級根本不須要知道這個消息。
那怎麼辦呢,難道是直接打電話溝通嗎?這個效率高是高,就是容易亂,這也就至關於界面塊之間的直接經過id調用。

Angular的做用域樹相似於傳統的組織架構樹,一個大型企業,通常都會有若干層級,近年來有不少管理的方法論,好比說組織架構的扁平化。

咱們能不能這樣:搞一個專門負責通信的機構,你們的消息都發給它,而後由它發給相關人員,其餘人員在理念上都是平級關係。

這就是一個很典型的訂閱發佈模式,接收方在這裏訂閱消息,發佈方在這裏發佈消息。這個過程能夠用這樣的圖形來表示:

應用內的事件總線

代碼寫起來也很簡單,把它作成一個公共模塊,就能夠被各類業務方調用了:

app.factory("EventBus", function() {
    var eventMap = {};

    var EventBus = {
        on : function(eventType, handler) {
            //multiple event listener
            if (!eventMap[eventType]) {
                eventMap[eventType] = [];
            }
            eventMap[eventType].push(handler);
        },

        off : function(eventType, handler) {
            for (var i = 0; i < eventMap[eventType].length; i++) {
                if (eventMap[eventType][i] === handler) {
                    eventMap[eventType].splice(i, 1);
                    break;
                }
            }
        },

        fire : function(event) {
            var eventType = event.type;
            if (eventMap && eventMap[eventType]) {
                for (var i = 0; i < eventMap[eventType].length; i++) {
                    eventMap[eventType][i](event);
                }
            }
        }
    };
    return EventBus;
});

 

事件訂閱代碼:

EventBus.on("someEvent", function(event) {
    // 這裏處理事件
    var c = event.data.a + event.data.b;
});

 

事件發佈代碼:

EventBus.fire({
    type: "someEvent",
    data: {
        aaa: 1,
        bbb: 2
    }
});

 

注意,若是在複雜的應用中使用事件總線,須要慎重規劃事件名,推薦使用業務路徑,好比:"portal.menu.selectedMenuChange",以免事件衝突。

小結

在本章,咱們學習了做用域相關的知識,以及它們之間傳遞數據的方式。做用域在整個Angular應用中造成了一棵樹,以$rootScope爲根部,開枝散葉。這棵樹獨立於DOM而存在,又與DOM相關聯。事件在整個樹上傳播,如蜂飛蝶舞。

整體來講,使用AngularJS對JavaScript的基本功是有必定要求的,由於這裏面大部分實現都依賴於純JavaScript語法,好比原型繼承的使用。若是對這一塊有充分的認識,理解Angular的做用域就會比較容易。

一個大型單頁應用,須要對部件的整合方式和通訊機制做良好的規劃,爲它們創建良好的秩序,這對於確保整個應用的穩定性是很是必要的。

首要問題不是自由,而是創建合法的公共秩序。人類能夠無自由而有秩序,但不能無秩序而有自由。——繆爾·亨廷頓

本章所涉及的全部Demo,參見在線演示地址

代碼庫

演講幻燈片下載:點這裏

相關文章
相關標籤/搜索