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的执行流程和源码分析,好了,欢乐的时光总是过得特别快,又到时候和大家讲拜拜!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值