发布订阅

angularjs 的发布订阅和其他的发布订阅实现上都是一致的,但是有一个关键的区别,angularjs 的 pub/sub 模式是依赖于 scope 树来向上和向下处理事件,而不是单个的一个中心处理所有的事件流。

当你 publish 一个事件的时候,可能有两种模式,沿着树向上或者向下传播。当向上传播时当前作用域和父辈作用域将会注意到,称为 emitting 一个事件。向下传播时当前作用域和子辈作用域会注意到,称为 broadcasting 一个事件。

上面提到的事件处理系统处理的是应用级别的事件由开发者创建的事件,而 dom 原生的事件例如点击等,这些事件都是由 angular.element 处理的。

angularjs 注册 subscribers 方式是使用 $on 方法,该方法接受一个事件名称和回调函数,subscribers 注册后会接受到 emitting 和 broadcasting 传来的事件,除了事件名字外,subscribers 对于事件没有任何限制。

  1. Scope.prototype.$on = function (eventName, listener) {
  2. const listeners = this.$$listeners[eventName];
  3. if (!listeners) {
  4. this.$$listeners[eventName] = listeners = [];
  5. }
  6. listeners.push(listener);
  7. };

注意到函数中使用的 listeners 始终使用的是顶层,我们想让每个作用域都互不干扰,所以我们需要在每个作用域都创建一个数组来覆盖掉顶层的 listeners ,在 $new 进行和 $$watchers 同样的操作即可。

$emit and $broadcast

和其他 pub 的实现一致。

  1. Scope.prototype.$emit = function (eventName) {
  2. const listeners = this.$$listeners[eventName] || [];
  3. _.forEach(listeners, function (listener) {
  4. listener();
  5. });
  6. };

注销 listeners

  1. Scope.prototype.$on = function (eventName, listener) {
  2. let listeners = this.$$listeners[eventName];
  3. if (!listeners) {
  4. this.$$listeners[eventName] = listeners = []; // 保证空数组和listeners是同一地址
  5. }
  6. listeners.push(listener);
  7. return function () {
  8. const index = listeners.indexOf(listener);
  9. if (index >= 0) {
  10. listeners[index] = null; // 防止顺序出现问题
  11. }
  12. };
  13. };
  14. Scope.prototype.$emit = function (eventName) {
  15. const additionalArgs = _.tail(arguments);
  16. return this.$$fireEventOnScope(eventName, additionalArgs);
  17. };
  18. Scope.prototype.$broadcast = function (eventName) {
  19. const additionalArgs = _.tail(arguments);
  20. return this.$$fireEventOnScope(eventName, additionalArgs);
  21. };
  22. Scope.prototype.$$fireEventOnScope = function (eventName, additionalArgs) {
  23. const event = { name: eventName }; // 包装一个事件对象,回调函数的第一个参数
  24. const listenerArgs = [event].concat(additionalArgs);
  25. const listeners = this.$$listeners[eventName] || [];
  26. let i = 0;
  27. while (i < listeners.length) {
  28. if (listeners[i] === null) {
  29. listeners.splice(i, 1); // 如果是null,那么指针不指向下一位
  30. } else {
  31. listeners[i].apply(null, listenerArgs);
  32. i++;
  33. }
  34. }
  35. return event;
  36. };

注销功能在注册时返回,这样就可以在数组中找到同一个函数去注销。

传递事件

  1. Scope.prototype.$emit = function (eventName) {
  2. const event = { name: eventName };
  3. const listenerArgs = [event].concat(_.tail(arguments));
  4. let scope = this;
  5. do {
  6. scope.$$fireEventOnScope(eventName, listenerArgs);
  7. scope = scope.$parent;
  8. } while (scope);
  9. return event;
  10. };
  11. Scope.prototype.$broadcast = function (eventName) {
  12. Scope.prototype.$broadcast = function (eventName) {
  13. const event = { name: eventName };
  14. const listenerArgs = [event].concat(_.tail(arguments));
  15. this.$$everyScope(function (scope) {
  16. scope.$$fireEventOnScope(eventName, listenerArgs);
  17. return true;
  18. });
  19. return event;
  20. };
  21. };

由于向下传递需要遍历一棵树,所以我们直接使用之前 digest 用到过的 everyScope 即可

包装事件对象

理想中的事件对象应该包含,当前作用域和出发作用域

  1. Scope.prototype.$emit = function (eventName) {
  2. const event = { name: eventName, targetScope: this };
  3. const listenerArgs = [event].concat(_.tail(arguments));
  4. let scope = this;
  5. do {
  6. event.currentScope = scope;
  7. scope.$$fireEventOnScope(eventName, listenerArgs);
  8. scope = scope.$parent;
  9. } while (scope);
  10. return event;
  11. };
  12. Scope.prototype.$broadcast = function (eventName) {
  13. const event = { name: eventName, targetScope: this };
  14. const listenerArgs = [event].concat(_.tail(arguments));
  15. this.$$everyScope(function (scope) {
  16. event.currentScope = scope;
  17. scope.$$fireEventOnScope(eventName, listenerArgs);
  18. return true;
  19. });
  20. return event;
  21. };