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属于语法解析功能,后续单独分析。