AngularJs scope事件機制$emit , $broadcast,$on廣播事件(源碼分析)

在angular scope中可以通過$on,$emit和$broadcast方法實現了自定義事件機制,實現數據共享,原理其實並不複雜,下面我會用例子來做分析,先看一段測試源碼:

    <div ng-controller="parentCtrl">
        <div ng-controller="selfCtrl">
            <a ng-click="click()">點擊</a>
            <div ng-controller="childCtrl"></div>
        </div>
        <div ng-controller="siblingCtrl"></div>
    </div>

<script>
var app = angular.module('myApp',[]);
      app.controller('selfCtrl', function ($scope) {
          $scope.click = function () {
              $scope.$broadcast('to-child', 'child');
              $scope.$emit('to-parent', 'parent');
          }
      });

      app.controller('parentCtrl', function ($scope) {
          $scope.$on('to-parent', function (event, data) {
              console.log('parentCtrl', data); //父級能得到值
          });
      });

      app.controller('childCtrl', function ($scope) {
          $scope.$on('to-child', function (event, data) {
              console.log('childCtrl', data); //子級能得到值
          });
      });

      app.controller('siblingCtrl', function ($scope) {
      });
</script>

當按了點擊之後,纔會向上和向下派發事件,當我沒按點擊之前,parentCtrl上,和childCtrl上都有註冊事件,我們從下往上看,
先看childCtrl scope:
在這裏插入圖片描述
id:表示作用域id。
listenerCount: 表示註冊監聽事件的總數包括子的監聽事件,key是事件名,value是監聽的總數(是個對象)。
listeners: 表示註冊監聽事件,key表示事件名,value是個回調函數(是個對象)。
在childCtrl上面只註冊監聽了一個to-child事件,從代碼中和圖中已經很明瞭了。

接下來看selfCtrl scope:
在這裏插入圖片描述
從代碼中可以看出selfCtrl 中並沒有註冊監聽事件,只有派發事件,還是按了點擊之後纔派發事件,所以它的listeners是空的,因爲它的子作用域childCtrl上有註冊to-child監聽事件,所以$$listenerCount的value值是1。

再來看看selfCtrl的兄弟 siblingCtrl的scope:
在這裏插入圖片描述
代碼中siblingCtrl controller裏面都是空的,並且又沒有子作用域,所以啥玩意沒有。

最後咱們來看看最外層parentCtrl 的scope:
在這裏插入圖片描述
其實上面還有個rootScope,rootScope的id爲1,這裏我就不截圖了,parentCtrl controller代碼中我註冊了個to-parent事件,listeners就這個事件,它的子selfCtrl上有個to-child事件,所以listenerCount 有兩個值。所以我們可以得知,listeners是當前作用域的註冊的監聽事件,listenerCount是它的後代所有的註冊的監聽的總值。好,知道了這些,我們再來看它的源碼就很簡單了。

1.$on方法註冊監聽事件

直接上源碼:

//這裏參數是註冊監聽的事件名和回調fn
 $on: function (name, listener) {
   var namedListeners = this.$$listeners[name];
   if (!namedListeners) {
     this.$$listeners[name] = namedListeners = [];
   }
    //監聽函數存到存值數組中
   namedListeners.push(listener);

   var current = this;
   do {
     //從子往父維護$$listenerCount的值
     if (!current.$$listenerCount[name]) {
       current.$$listenerCount[name] = 0;
     }
     current.$$listenerCount[name]++;
   } while ((current = current.$parent));

   var self = this;
   //當執行了回調後,取消監聽函數
   return function () {
     //判斷存儲數組中監聽函數是否存在
     var indexOfListener = namedListeners.indexOf(listener);
     if (indexOfListener !== -1) {
       //從存儲中刪除該監聽函數
       delete namedListeners[indexOfListener];
       //刪除完之後,從子往父維護$$listenerCount的值
       decrementListenerCount(self, 1, name);
     }
   };
 }

一開始listenerCount,listeners都是空對象,是在創建scope的時候創建的,剛註冊的時候會先判下有沒有註冊這個事件,如果沒有,就以key爲事件名,value開始設置爲一個空數組,然後把回調函數放進這個數組裏,然後會判斷listenerCount[key]有沒有值,沒有就設置listenerCount[key]=1;然後往上找父scope,在每個scope都設置listenerCount[key]=1;當然這個是剛開始的時候,之後listenerCount[key]++了,當收到了派發事件執行回調,取消監聽函數,有了之前的測試代碼分析,這裏應該很清楚了,其實這裏就是更新listenerCount,listeners這兩個對象。

2.$emit向上冒泡傳遞事件

$emit 發出,放射的意思,就和火箭一樣,肯定是向上傳播了,通過scope不斷向父scope傳遞消息,這裏和js中的向上冒泡有點相似,也是從下往上傳播,還是直接上源碼吧:

 //兩參數爲傳播的事件名和值
 $emit: function (name, args) {
    var empty = [],
      namedListeners,
      scope = this,
      //默認阻止冒泡是爲false的
      stopPropagation = false,
     // 初始化event對象,也就傳遞給監聽函數的event對象
      event = {
        name: name,
        targetScope: scope,//這裏是最初始的scope
        stopPropagation: function () {
          stopPropagation = true;
        },
        //阻止默認事件默認是不阻止的值爲false,阻止之後爲true
        preventDefault: function () {
          event.defaultPrevented = true;
        },
        defaultPrevented: false
      },
      //傳遞給監聽函數的參數event對象和要傳的值放進一個數組裏面
      listenerArgs = concat([event], arguments, 1),
      i, length;

    do {
     // 循環處理作用域上的監聽函數(從子到父逐個找註冊的監聽事件)
      namedListeners = scope.$$listeners[name] || empty;
      event.currentScope = scope;//當前的作用域
      for (i = 0, length = namedListeners.length; i < length; i++) {
        // 如果已註銷監聽器,事件取消
        if (!namedListeners[i]) {
          namedListeners.splice(i, 1);
          i--;
          length--;
          continue;
        }
        try {
          // 執行當前scope的回調
          namedListeners[i].apply(null, listenerArgs);
        } catch (e) {
          $exceptionHandler(e);
        }
      }
      //如果回調設置了stopPropagation爲true,那麼終止冒泡過程
      if (stopPropagation) {
        break;
      }
      // 向上遍歷父作用域
      scope = scope.$parent;
    } while (scope);
// 處理完監聽函數後,去除作用域引用
    event.currentScope = null;

    return event;
  }

既然是冒泡,當然就有阻止冒泡的方法,angular在會傳遞給監聽函數一個event對象,可以通過event.stopPropagation方法來做到這一點,$emit的原理不斷循環處理父級作用域上的監聽函數,源碼中做了很多優化處理而已。
值得留意的有以下幾個地方:

  1. 處理回調函數中空元素的邏輯。首先想想什麼情況下才會出現這種情況呢?難道遍歷中會發生事件的註銷嗎?答案是:是的,在回調函數就有可能把它自己給註銷了。當只需要調用一次某個回調函數的時候,就會出現這種情況。
  2. 在以此遍歷每個回調函數的時候,如果第一個回調函數改變了event或者是其它參數,後續的回調函數就能夠發現並根據參數作出合適的處理。,比如第一個回調如果計算得到了一個值,就可以將該值放入到參數中供後續的回調函數使用。
  3. preventDefault這個flag並沒有在遍歷過程中被使用,這個flag可以在回調函數中使用,根據其值執行不同的業務邏輯。也可以在其它需要的地方使用,因爲它也是返回的事件對象上的一個屬性,這一點和stopPropagation不一樣,後者並不是事件對象上的屬性。
  4. 返回event對象之前,會清空其中定義的currentScope屬性。因爲該屬性隨着遍歷會發生變化,因此將它暴露出去沒有意義,在返回之前清空。
  5. 檢測是否stopPropagation的邏輯發生在循環當前scope的所有回調之後。這樣做能夠保證當前scope上的所有回調都會被執行
3.$broadcast向下廣播傳遞事件

和$emit一樣需要向其他作用域傳遞消息,這裏的傳遞的目標作用域不再是父scope,而是所有的子scope,避免深層次的循環嵌套,採用深度優先算法遍歷作用域樹,從而達到廣播的效果,直接上源碼:

	//兩參數爲傳播的事件名和值
	$broadcast: function (name, args) {
	var target = this,
	//target是最初始的scope
	  current = target,
	  next = target,
	  // 初始化event對象,也就傳遞給監聽函數的event對象
	  event = {
	    name: name,
	    targetScope: target,
	    //阻止默認事件默認是不阻止的值爲false,阻止之後爲true
	    preventDefault: function () {
	      event.defaultPrevented = true;
	    },
	    defaultPrevented: false
	  };
	//因爲是從父scope往子傳播,如果父的$$listenerCount都沒有值,那麼子$$listeners肯定是沒有值的,這裏做了個優化!
	if (!target.$$listenerCount[name]) return event;
	//傳遞給監聽函數的參數event對象和要傳的值放進一個數組裏面
	var listenerArgs = concat([event], arguments, 1),
	  listeners, i, length;
	
	//down while you can, then up and next sibling or up and next sibling until back at root
	while ((current = next)) {
	  event.currentScope = current;
	  listeners = current.$$listeners[name] || [];
	  for (i = 0, length = listeners.length; i < length; i++) {
	    //和$emit一樣,如果已註銷監聽器,事件取消
	    if (!listeners[i]) {
	      listeners.splice(i, 1);
	      i--;
	      length--;
	      continue;
	    }
	
	    try {
	     // 執行當前scope的回調
	      listeners[i].apply(null, listenerArgs);
	    } catch (e) {
	      $exceptionHandler(e);
	    }
	  }
	   // 和digest循環中一樣實現了深度優先遍歷,其中利用$$listenerCount做了性能優化(先找子,沒有的話再找兄弟,再沒有回到父)
	  if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
	      (current !== target && current.$$nextSibling)))) {
	    while (current !== target && !(next = current.$$nextSibling)) {
	      current = current.$parent;
	    }
	  }
	}
	event.currentScope = null;
	return event;
	}
	}

原理和$emit沒有什麼區別,主要不同點在於, 沒有stopPropagation,遍歷的方式爲深度優先遍歷,這裏listenerCount[name]值大於0纔會遍歷。這也算是性能上的優化吧。否則在沒有註冊回調函數的情況下,每次都遍歷只會浪費性能。

到這裏,scope事件執行機制就講完了(如果有不對的地方,歡迎指出,謝謝!)下一篇文件我會講下angular的執行流程和源碼分析,好了,歡樂的時光總是過得特別快,又到時候和大家講拜拜!!

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