深入理解AngularJs-scope(二)
深入理解AngularJs-scope(一)中,我们对AngularJs的脏检测及其触发、异步任务队列进行了学习。紧接上一篇文章 深入理解AngularJs-scope(一),我们来看看scope对以下两个特性的实现。
- scope的继承机制和 isolated scope;
- 依赖于scope的事件系统: $on, $broadcast, $emit;
scope的继承机制
在上一篇文章中,我们创建了scope类,并在scope.prototype上实现了脏检测和异步任务相关的各个方法。
现在,我们来看看AngularJs中,scope之间是如何通过继承联系在一起的,如何从parentScope上获取properties。实际上,得益于javascript的原型继承机制,要实现scope的继承相当的简单,代码如下:
1 Scope.prototype.$new = function(isolated, parent) { 2 var child; 3 parent = parent || this; 4 5 if(isolated) { 6 child = new Scope(); 7 child.$root = parent.$root; 8 child.$$asyncQueue = parent.$$asyncQueue; 9 child.$$postDigestQueue = parent.$$postDigestQueue; 10 child.$$applyAsyncQueue = parent.$$applyAsyncQueue; 11 } else { 12 var ChildScope = function() {}; 13 ChildScope.prototype = this; 14 child = new ChildScope(); 15 } 16 17 parent.$$children.push(child); 18 19 child.$$watchers = []; // shadow这个prop,使成为每个scope独立拥有这个prop 20 child.$$listeners = {}; // shadow这个prop, 存储包含自定义事件键值对的对象 21 child.$$children = []; // shadow这个prop,使成为每个scope独立拥有这个prop 22 child.$parent = parent; // 缓存parentScope, 以便让scope上的其他method能够使用它,比如$destroy 23 24 return child; 25 };
在我们使用AngularJs进行开发时,$new方法的调用无处不在。大部分情况下并不需要我们手动调用,只是指令自己做了创建scope的工作。
$new方法有两个参数:
isolated-布尔值,表示新建的scope是不是 isolated scope(孤立的scope)。
parent-支持传入一个其他scope来作为 AngularJs scope机制中的parentScope。
AngularJs中的scope分为两种,一种是普通scope,如上面代码13行所示,普通scope的prototype指向parentScope, 故能够通过原型链获取到parentScope上的properties。
另一种是isolated(孤立) scope, isolated是通过 Scope构造函数创建,它的protorype是指向scope构造函数的,并不是parentScope,所以不能从原型链访问parentScope的properties。
代码19行至22对新scope的$$watchers、$$listeners、$$children、$parent进行了初始化,因为这些属性是每个scope实例自己拥有、自己维护的。
scope的事件系统:订阅/发布
AngularJs 也为开发者提供了一套事件系统供开发者进行事件的绑定和触发,基于 publish/subscribe 设计模式。其中包含3个核心方法:$on, $emit, $broadcast。
$on: 在scope上绑定自定义事件,即向scope的$$listeners数组中插入listener回调函数。返回事件销毁函数。
1 Scope.prototype.$on = function(eventName, listener) { 2 var listeners = this.$$listeners[eventName]; 3 4 if(!listeners) { 5 this.$$listeners[eventName] = listeners = []; 6 } 7 8 listeners.push(listener); 9 10 return function(eventName) { 11 var index = listeners.indexOf(listener); 12 13 if(index >= 0) { 14 listeners[index] = null; 15 } 16 }; 17 };
$emit: 沿着scope -> parentScope 向上发射事件,执行对应的回调函数。
Scope.prototype.$emit = function(eventName) { var propagationStopped = false; var event = { name: eventName, targetScope: this, stopPropagation: function() { propagationStopped = true; }, preventDefault: function() { event.defaultPrevented = true; } }; // 把event和additionalArgs拼接成新数组,通过apply方法传入listener, 使参数获取方式正常 var listenerArgs = [event].concat(_.tail(arguments)); var scope = this; do { event.currentScope = scope; scope.$$fireEventOnScope(eventName, listenerArgs); scope = scope.$parent; // 通过改变scope引用 实现向上传播的关键代码 } while (scope && !propagationStopped); event.currentScope = null; return event; };
$broadcast: 向下广播事件,并触发对应的回调函数。$broadcast有一点特殊,一旦开始向下广播,就不能中断。
1 Scope.prototype.$broadcast = function(eventName) { 2 var event = { 3 name: eventName, 4 targetScope: this, 5 preventDefault: function() { 6 event.defaultPrevented = true; 7 } 8 }; 9 10 // 把event和additionalArgs拼接成新数组,通过apply方法传入listener, 使参数获取方式正常 11 var listenerArgs = [event].concat(_.tail(arguments)); 12 13 this.$$everyScope(function(scope) { 14 event.currentScope = scope; 15 scope.$$fireEventOnScope(eventName, listenerArgs); 16 return true; 17 }); 18 19 event.currentScope = null; 20 21 return event; 22 };
这两篇用到的工具函数我都放在后面,由于事件系统的代码比较简单,就不再做过多说明。
工具方法1: $$fileEventOnScope
1 /* $emit 和 $broadcast中 提取出的 fire event listener函数 2 angularjs源码没有这个方法,其中只是重复了这些代码, 本书作者提出了重复代码 3 */ 4 Scope.prototype.$$fireEventOnScope = function(eventName, listenerArgs) { 5 6 var listeners = this.$$listeners[eventName] || []; 7 var i = 0; 8 9 while(i < listeners.length) { 10 if(listeners[i] === null) { 11 listeners.splice(i, 1); 12 } else { 13 try { 14 listeners[i].apply(null, listenerArgs); 15 } catch(e) { 16 console.error(e); 17 } 18 i++; 19 } 20 } 21 22 return event; 23 };
工具方法2: $$everyScope
1 /* 为使$digest循环能够递归child scope上的watchers的工具方法 2 这个方法还用于实现$broadcast 3 */ 4 Scope.prototype.$$everyScope = function(fn) { 5 if(fn(this)) { 6 return this.$$children.every(function(child) { 7 return child.$$everyScope(fn); 8 }); 9 } else { 10 return false; 11 } 12 };
总结:
这两篇文章提供了与scope相关的脏检测,$watch, 异步任务,继承机制,事件系统的代码及一点补充分析。弄明白了这些机制是如何实现的,当你在开发工作中用到这些东西时,一定会多一份自信,多一份游刃有余。希望这两篇文章能够帮助到正在使用angular1.x开发的朋友。如有错误,请不吝指出~!谢谢~