Angularjs 留存(三)

 

 

PS:AngularJS留存系列主要是用來記錄一些自己在學習其他文章時覺得有必要留存一下的fortune!

文章可能會很長(默認每篇17個小的知識點),但是我們可以使用Ctrl + F 來查找啦!

 

1、$parse/$eval和$observe/$watch如何區分

 

 

$parse和$eval

 

首先,$parse跟$eval都是用來解析表達式的, 但是$parse是作爲一個單獨的服務存在的。$eval是作爲scope的方法來使用的。
$parse典型的使用是放在設置字符串表達式映射在真實對象上的值。也可以從$parse上直接獲取到表達式對應的值。

var getter = $parse('user.name'); 
var setter = getter.assign; 
setter(scope, 'new name');
getter(context, locals) // 傳入作用域,返回值
setter(scope,'new name') // 修改映射在scope上的屬性的值爲‘new value’

$eval 即scope.$eval,是執行當前作用域下的表達式,如:scope.$eval('a+b'); 而這個裏的a,b是來自 scope = {a: 2, b:3};
看看源碼它的實現是

$eval: function(expr, locals) {
    return $parse(expr)(this, locals);
},

可以找到它也是基於$parse,不過它的參數已經被固定爲this,就是當前的scope,所以$eval只是在$parse基礎上的封裝而已,是一種$parse快捷的API。

2、AngularJS內幕詳解之 Directive

controller 這是一個指令的控制器實例。

 

指令可以有控制器,這說得通是因爲指令可以創建 scope 。該控制器在所有的同一 scope 的指令中共享,同時可以作爲 link 函數的第四個參數被訪問到。在同一層級的scope上,這些控制器是指令間的一個可用的通信信道,也可能包含指令自身。

  • controllerAs 這是在模板中使用的 controller 別名

使用控制器別名允許你在模板裏面引用控制器,因爲他在 scope 中是可見的。

  • require 如果你沒有鏈接其他的指令到某個元素上會拋出一個錯誤。

 

 

 

引入其他指令並注入到控制器中,並作爲當前指令的鏈接函數的第四個參數。require使用字符串或數組元素來傳入指令。如果是數組,注入的參數是一個相應順序的數組。如果這樣的指令沒有被找到,或者該指令沒有控制器, 就會報錯。 require參數可以加一些前綴:

  • (沒有前綴)如果沒有前綴,指令將會在自身所提供的控制器中進行查找,如果沒有找到任何控制器就拋出一個錯誤。
  • ? 如果在當前指令中沒有找到所需要的控制器,會將null作爲傳給link函數的第四個參數。
  • ^ 如果添加了^前綴,指令會在上游的指令鏈中查找require參數所指定的控制器。
  • ?^ 將前面兩個選項的行爲組合起來,我們可選擇地加載需要的指令並在父指令鏈中進行查找。

 

在我們的工作中,如果我們的指令需要依賴其他的指令時,require 很有用。 舉個例子, 你或許有個 dropdown 指令,他依賴一個 list-view 指令, 或者一個錯誤彈框的指令依賴一個錯誤消息指令。在下面的例子中,反過來說,定義一個 needs-model 指令, 如果他沒有找到依賴的 ng-model 就會拋出一個錯誤 —— 因爲 needs-model指令使用了那個指令,或者某種程度上取決於他在元素上可用。

angular.module('PonyDeli').directive(‘needsModel’, function () {
  return {
    require: 'ngModel’,
  }
});
<div needs-model ng-model=’foo’></div>

3、不請自來的新作用域

 

 

在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等等,規律可以總結,也就是那些會動態創建一塊界面的東西,都是自帶一級作用域。

4、作用域上的事件

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

提到事件,可能很多人想到的都是DOM事件,其實DOM事件只存在於上層,而且沒有業務含義,如果我們想要傳遞一個明確的業務消息,就需要使用業務事件。這種所謂的業務事件,其實就是一種消息的傳遞。

假設有如圖所示的應用:

事件的傳遞

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

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

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

事件的傳遞

對於這種事件的傳播方式,可以有個類似的比喻:

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

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

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

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

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

5、事件的接收與阻止

無論是$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;
    }
});

 

 

 

6、事件總線

 

在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",以避免事件衝突。

7.transclude()函數

 

在一個模板裏,ng-transclude指令只能被使用一次.所以,如果需要重複使用那一坨,需要使用transclude()函數,這個函數分別可以出現在以下三個地方:

1.控制器裏,通過依賴注入 $transclude

2.link屬性的函數的第五個參數.只要順序是在第五個,名字取什麼不重要,一般就取transclude

3.compile屬性的函數的第三個參數.只要順序是在第三個,名字取什麼不重要,一般就取transclude

transclude()的用法是進階話題,需要理解angular的編譯,鏈接,指令的作用域.並且,只有ng-repeat這類指令纔會用到.故這裏先放一放,日後再深入研究.

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章