angularjs 源码解析之scope
简介
在ng的生态中scope处于一个核心的地位,ng对外宣称的双向绑定的底层其实就是scope实现的,本章主要对scope的watch机制、继承性以及事件的实现作下分析。
监听
1.$watch
1.1使用
//$watch:function(watchExp,listener,objectEquality)
varunwatch=$scope.$watch('aa',function(){},isEqual);
使用过angular的会经常这上面这样的代码,俗称“手动”添加监听,其他的一些都是通过插值或者directive自动地添加监听,但是原理上都一样。
1.2源码分析
function(watchExp,listener,objectEquality){ varscope=this, //将可能的字符串编译成fn get=compileToFn(watchExp,'watch'), array=scope.$$watchers, watcher={ fn:listener, last:initWatchVal,//上次值记录,方便下次比较 get:get, exp:watchExp, eq:!!objectEquality//配置是引用比较还是值比较 }; lastDirtyWatch=null; if(!isFunction(listener)){ varlistenFn=compileToFn(listener||noop,'listener'); watcher.fn=function(newVal,oldVal,scope){listenFn(scope);}; } if(!array){ array=scope.$$watchers=[]; } //之所以使用unshift不是push是因为在$digest中watchers循环是从后开始 //为了使得新加入的watcher也能在当次循环中执行所以放到队列最前 array.unshift(watcher); //返回unwatchFn,取消监听 returnfunctionderegisterWatch(){ arrayRemove(array,watcher); lastDirtyWatch=null; }; }
从代码看$watch还是比较简单,主要就是将watcher保存到$$watchers数组中
2.$digest
当scope的值发生改变后,scope是不会自己去执行每个watcher的listenerFn,必须要有个通知,而发送这个通知的就是$digest
2.1源码分析
整个$digest的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirtycheckloop)中,循环后也有些次要的代码,如postDigestQueue的处理等就不作详细分析了。
脏值检查循环,意思就是说只要还有一个watcher的值存在更新那么就要运行一轮检查,直到没有值更新为止,当然为了减少不必要的检查作了一些优化。
代码:
//进入$digest循环打上标记,防止重复进入 beginPhase('$digest'); lastDirtyWatch=null; //脏值检查循环开始 do{ dirty=false; current=target; //asyncQueue循环省略 traverseScopesLoop: do{ if((watchers=current.$$watchers)){ length=watchers.length; while(length--){ try{ watch=watchers[length]; if(watch){ //作更新判断,是否有值更新,分解如下 //value=watch.get(current),last=watch.last //value!==last如果成立,则判断是否需要作值判断watch.eq?equals(value,last) //如果不是值相等判断,则判断NaN的情况,即NaN!==NaN if((value=watch.get(current))!==(last=watch.last)&& !(watch.eq ?equals(value,last) :(typeofvalue==='number'&&typeoflast==='number' &&isNaN(value)&&isNaN(last)))){ dirty=true; //记录这个循环中哪个watch发生改变 lastDirtyWatch=watch; //缓存last值 watch.last=watch.eq?copy(value,null):value; //执行listenerFn(newValue,lastValue,scope) //如果第一次执行,那么lastValue也设置为newValue watch.fn(value,((last===initWatchVal)?value:last),current); //...watchLog省略 if(watch.get.$$unwatch)stableWatchesCandidates.push({watch:watch,array:watchers}); } //这边就是减少watcher的优化 //如果上个循环最后一个更新的watch没有改变,即本轮也没有新的有更新的watch //那么说明整个watches已经稳定不会有更新,本轮循环就此结束,剩下的watch就不用检查了 elseif(watch===lastDirtyWatch){ dirty=false; breaktraverseScopesLoop; } } }catch(e){ clearPhase(); $exceptionHandler(e); } } } //这段有点绕,其实就是实现深度优先遍历 //A->[B->D,C->E] //执行顺序A,B,D,C,E //每次优先获取第一个child,如果没有那么获取nextSibling兄弟,如果连兄弟都没了,那么后退到上一层并且判断该层是否有兄弟,没有的话继续上退,直到退到开始的scope,这时next==null,所以会退出scopes的循环 if(!(next=(current.$$childHead|| (current!==target&¤t.$$nextSibling)))){ while(current!==target&&!(next=current.$$nextSibling)){ current=current.$parent; } } }while((current=next)); //breaktraverseScopesLoop直接到这边 //判断是不是还处在脏值循环中,并且已经超过最大检查次数ttl默认10 if((dirty||asyncQueue.length)&&!(ttl--)){ clearPhase(); throw$rootScopeMinErr('infdig', '{0}$digest()iterationsreached.Aborting!\n'+ 'Watchersfiredinthelast5iterations:{1}', TTL,toJson(watchLog)); } }while(dirty||asyncQueue.length);//循环结束 //标记退出digest循环 clearPhase();
上述代码中存在3层循环
第一层判断dirty,如果有脏值那么继续循环
do{
//...
}while(dirty)
第二层判断scope是否遍历完毕,代码翻译了下,虽然还是绕但是能看懂
do{
//....
if(current.$$childHead){
next= current.$$childHead;
}elseif(current!==target&¤t.$$nextSibling){
next=current.$$nextSibling;
}
while(!next&¤t!==target&&!(next=current.$$nextSibling)){
current=current.$parent;
}
}while(current=next);
第三层循环scope的watchers
length=watchers.length;
while(length--){
try{
watch=watchers[length];
//...省略
}catch(e){
clearPhase();
$exceptionHandler(e);
}
}
3.$evalAsync
3.1源码分析
$evalAsync用于延迟执行,源码如下:
function(expr){ if(!$rootScope.$$phase&&!$rootScope.$$asyncQueue.length){ $browser.defer(function(){ if($rootScope.$$asyncQueue.length){ $rootScope.$digest(); } }); } this.$$asyncQueue.push({scope:this,expression:expr}); }
通过判断是否已经有dirtycheck在运行,或者已经有人触发过$evalAsync
if(!$rootScope.$$phase&&!$rootScope.$$asyncQueue.length) $browser.defer就是通过调用setTimeout来达到改变执行顺序 $browser.defer(function(){ //... });
如果不是使用defer,那么
function(exp){ queue.push({scope:this,expression:exp}); this.$digest(); } scope.$evalAsync(fn1); scope.$evalAsync(fn2); //这样的结果是 //$digest()>fn1>$digest()>fn2 //但是实际需要达到的效果:$digest()>fn1>fn2
上节$digest中省略了了async的内容,位于第一层循环中
while(asyncQueue.length){ try{ asyncTask=asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); }catch(e){ clearPhase(); $exceptionHandler(e); } lastDirtyWatch=null; }
简单易懂,弹出asyncTask进行执行。
不过这边有个细节,为什么这么设置呢?原因如下,假如在某次循环中执行到watchX时新加入1个asyncTask,此时会设置lastDirtyWatch=watchX,恰好该task执行会导致watchX后续的一个watch执行出新值,如果没有下面的代码,那么下个循环到lastDirtyWatch(watchX)时便跳出循环,并且此时dirty==false。
lastDirtyWatch=null;
还有这边还有一个细节,为什么在第一层循环呢?因为具有继承关系的scope其$$asyncQueue是公用的,都是挂载在root上,故不需要在下一层的scope层中执行。
2.继承性
scope具有继承性,如$parentScope,$childScope两个scope,当调用$childScope.fn时如果$childScope中没有fn这个方法,那么就是去$parentScope上查找该方法。
这样一层层往上查找直到找到需要的属性。这个特性是利用javascirpt的原型继承的特点实现。
源码:
function(isolate){ varChildScope, child; if(isolate){ child=newScope(); child.$root=this.$root; //isolate的asyncQueue及postDigestQueue也都是公用root的,其他独立 child.$$asyncQueue=this.$$asyncQueue; child.$$postDigestQueue=this.$$postDigestQueue; }else{ if(!this.$$childScopeClass){ this.$$childScopeClass=function(){ //这里可以看出哪些属性是隔离独有的,如$$watchers,这样就独立监听了, this.$$watchers=this.$$nextSibling= this.$$childHead=this.$$childTail=null; this.$$listeners={}; this.$$listenerCount={}; this.$id=nextUid(); this.$$childScopeClass=null; }; this.$$childScopeClass.prototype=this; } child=newthis.$$childScopeClass(); } //设置各种父子,兄弟关系,很乱! child['this']=child; child.$parent=this; child.$$prevSibling=this.$$childTail; if(this.$$childHead){ this.$$childTail.$$nextSibling=child; this.$$childTail=child; }else{ this.$$childHead=this.$$childTail=child; } returnchild; }
代码还算清楚,主要的细节是哪些属性需要独立,哪些需要基础下来。
最重要的代码:
this.$$childScopeClass.prototype=this;
就这样实现了继承。
3.事件机制
3.1$on
function(name,listener){ varnamedListeners=this.$$listeners[name]; if(!namedListeners){ this.$$listeners[name]=namedListeners=[]; } namedListeners.push(listener); varcurrent=this; do{ if(!current.$$listenerCount[name]){ current.$$listenerCount[name]=0; } current.$$listenerCount[name]++; }while((current=current.$parent)); varself=this; returnfunction(){ namedListeners[indexOf(namedListeners,listener)]=null; decrementListenerCount(self,1,name); }; }
跟$wathc类似,也是存放到数组--namedListeners。
还有不一样的地方就是该scope和所有parent都保存了一个事件的统计数,广播事件时有用,后续分析。
varcurrent=this; do{ if(!current.$$listenerCount[name]){ current.$$listenerCount[name]=0; } current.$$listenerCount[name]++; }while((current=current.$parent));
3.2$emit
$emit是向上广播事件。源码:
function(name,args){ varempty=[], namedListeners, scope=this, stopPropagation=false, event={ name:name, targetScope:scope, stopPropagation:function(){stopPropagation=true;}, preventDefault:function(){ event.defaultPrevented=true; }, defaultPrevented:false }, 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++){ //当监听remove以后,不会从数组中删除,而是设置为null,所以需要判断 if(!namedListeners[i]){ namedListeners.splice(i,1); i--; length--; continue; } try{ namedListeners[i].apply(null,listenerArgs); }catch(e){ $exceptionHandler(e); } } //停止传播时return if(stopPropagation){ event.currentScope=null; returnevent; } //emit是向上的传播方式 scope=scope.$parent; }while(scope); event.currentScope=null; returnevent; }
3.3$broadcast
$broadcast是向内传播,即向child传播,源码:
function(name,args){ vartarget=this, current=target, next=target, event={ name:name, targetScope:target, preventDefault:function(){ event.defaultPrevented=true; }, defaultPrevented:false }, listenerArgs=concat([event],arguments,1), listeners,i,length; while((current=next)){ event.currentScope=current; listeners=current.$$listeners[name]||[]; for(i=0,length=listeners.length;i<length;i++){ //检查是否已经取消监听了 if(!listeners[i]){ listeners.splice(i,1); i--; length--; continue; } try{ listeners[i].apply(null,listenerArgs); }catch(e){ $exceptionHandler(e); } } //在digest中已经有过了 if(!(next=((current.$$listenerCount[name]&¤t.$$childHead)|| (current!==target&¤t.$$nextSibling)))){ while(current!==target&&!(next=current.$$nextSibling)){ current=current.$parent; } } } event.currentScope=null; returnevent; }
其他逻辑比较简单,就是在深度遍历的那段代码比较绕,其实跟digest中的一样,就是多了在路径上判断是否有监听,current.$$listenerCount[name],从上面$on的代码可知,只要路径上存在child有监听,那么该路径头也是有数字的,相反如果没有说明该路径上所有child都没有监听事件。
if(!(next=((current.$$listenerCount[name]&¤t.$$childHead)|| (current!==target&¤t.$$nextSibling)))){ while(current!==target&&!(next=current.$$nextSibling)){ current=current.$parent; } }
传播路径:
Root>[A>[a1,a2],B>[b1,b2>[c1,c2],b3]]
Root>A>a1>a2>B>b1>b2>c1>c2>b3
4.$watchCollection
4.1使用示例
$scope.names=['igor','matias','misko','james']; $scope.dataCount=4; $scope.$watchCollection('names',function(newNames,oldNames){ $scope.dataCount=newNames.length; }); expect($scope.dataCount).toEqual(4); $scope.$digest(); expect($scope.dataCount).toEqual(4); $scope.names.pop(); $scope.$digest(); expect($scope.dataCount).toEqual(3);
4.2源码分析
function(obj,listener){ $watchCollectionInterceptor.$stateful=true; varself=this; varnewValue; varoldValue; varveryOldValue; vartrackVeryOldValue=(listener.length>1); varchangeDetected=0; varchangeDetector=$parse(obj,$watchCollectionInterceptor); varinternalArray=[]; varinternalObject={}; varinitRun=true; varoldLength=0; //根据返回的changeDetected判断是否变化 function$watchCollectionInterceptor(_value){ //... returnchangeDetected; } //通过此方法调用真正的listener,作为代理 function$watchCollectionAction(){ } returnthis.$watch(changeDetector,$watchCollectionAction); }
主脉络就是上面截取的部分代码,下面主要分析$watchCollectionInterceptor和$watchCollectionAction
4.3$watchCollectionInterceptor
function$watchCollectionInterceptor(_value){ newValue=_value; varnewLength,key,bothNaN,newItem,oldItem; if(isUndefined(newValue))return; if(!isObject(newValue)){ if(oldValue!==newValue){ oldValue=newValue; changeDetected++; } }elseif(isArrayLike(newValue)){ if(oldValue!==internalArray){ oldValue=internalArray; oldLength=oldValue.length=0; changeDetected++; } newLength=newValue.length; if(oldLength!==newLength){ changeDetected++; oldValue.length=oldLength=newLength; } for(vari=0;i<newLength;i++){ oldItem=oldValue[i]; newItem=newValue[i]; bothNaN=(oldItem!==oldItem)&&(newItem!==newItem); if(!bothNaN&&(oldItem!==newItem)){ changeDetected++; oldValue[i]=newItem; } } }else{ if(oldValue!==internalObject){ oldValue=internalObject={}; oldLength=0; changeDetected++; } newLength=0; for(keyinnewValue){ if(hasOwnProperty.call(newValue,key)){ newLength++; newItem=newValue[key]; oldItem=oldValue[key]; if(keyinoldValue){ bothNaN=(oldItem!==oldItem)&&(newItem!==newItem); if(!bothNaN&&(oldItem!==newItem)){ changeDetected++; oldValue[key]=newItem; } }else{ oldLength++; oldValue[key]=newItem; changeDetected++; } } } if(oldLength>newLength){ changeDetected++; for(keyinoldValue){ if(!hasOwnProperty.call(newValue,key)){ oldLength--; deleteoldValue[key]; } } } } returnchangeDetected; }
1).当值为undefined时直接返回。
2).当值为普通基本类型时直接判断是否相等。
3).当值为类数组(即存在length属性,并且value[i]也成立称为类数组),先没有初始化先初始化oldValue
if(oldValue!==internalArray){ oldValue=internalArray; oldLength=oldValue.length=0; changeDetected++; }
然后比较数组长度,不等的话记为已变化changeDetected++
if(oldLength!==newLength){ changeDetected++; oldValue.length=oldLength=newLength; }
再进行逐个比较
for(vari=0;i<newLength;i++){ oldItem=oldValue[i]; newItem=newValue[i]; bothNaN=(oldItem!==oldItem)&&(newItem!==newItem); if(!bothNaN&&(oldItem!==newItem)){ changeDetected++; oldValue[i]=newItem; } }
4).当值为object时,类似上面进行初始化处理
if(oldValue!==internalObject){ oldValue=internalObject={}; oldLength=0; changeDetected++; }
接下来的处理比较有技巧,但凡发现newValue多的新字段,就在oldLength加1,这样oldLength只加不减,很容易发现newValue中是否有新字段出现,最后把oldValue中多出来的字段也就是newValue中删除的字段给移除就结束了。
newLength=0; for(keyinnewValue){ if(hasOwnProperty.call(newValue,key)){ newLength++; newItem=newValue[key]; oldItem=oldValue[key]; if(keyinoldValue){ bothNaN=(oldItem!==oldItem)&&(newItem!==newItem); if(!bothNaN&&(oldItem!==newItem)){ changeDetected++; oldValue[key]=newItem; } }else{ oldLength++; oldValue[key]=newItem; changeDetected++; } } } if(oldLength>newLength){ changeDetected++; for(keyinoldValue){ if(!hasOwnProperty.call(newValue,key)){ oldLength--; deleteoldValue[key]; } } }
4.4$watchCollectionAction
function$watchCollectionAction(){ if(initRun){ initRun=false; listener(newValue,newValue,self); }else{ listener(newValue,veryOldValue,self); } //trackVeryOldValue=(listener.length>1)查看listener方法是否需要oldValue //如果需要就进行复制 if(trackVeryOldValue){ if(!isObject(newValue)){ veryOldValue=newValue; }elseif(isArrayLike(newValue)){ veryOldValue=newArray(newValue.length); for(vari=0;i<newValue.length;i++){ veryOldValue[i]=newValue[i]; } }else{ veryOldValue={}; for(varkeyinnewValue){ if(hasOwnProperty.call(newValue,key)){ veryOldValue[key]=newValue[key]; } } } } }
代码还是比较简单,就是调用listenerFn,初次调用时oldValue==newValue,为了效率和内存判断了下listener是否需要oldValue参数
5.$eval&$apply
$eval:function(expr,locals){ return$parse(expr)(this,locals); }, $apply:function(expr){ try{ beginPhase('$apply'); returnthis.$eval(expr); }catch(e){ $exceptionHandler(e); }finally{ clearPhase(); try{ $rootScope.$digest(); }catch(e){ $exceptionHandler(e); throwe; } } }
$apply最后调用$rootScope.$digest(),所以很多书上建议使用$digest(),而不是调用$apply(),效率要高点。
主要逻辑都在$parse属于语法解析功能,后续单独分析。