JavaScript   发布时间:2022-04-16  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了angularjs 源码解析之scope大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

简介

在Ng的生态中scope处于一个核心的地位,ng对外宣称的双向绑定的底层其实就是scope实现的,本章主要对scope的watch机制、继承性以及事件的实现作下分析。

监听

1. $watch

// $watch: function(watchExp,listener,objectEquality)

var unwatch = $scope.$watch('aa',function () {},isEqual);

使用过angular的会经常这上面这样的代码,俗称“手动”添加监听,其他的一些都是通过插值或者directive自动地添加监听,但是原理上都一样。

jectEquality) { var scope = 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)) {
var listenFn = 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,取消监听
return function deregisterWatch() {
arrayRemove(array,watcher);
lastDirtyWatch = null;
};
}

从代码看 $watch 还是比较简单,主要就是将 watcher 保存到 $$watchers 数组中

2. $digest

当 scope 的值发生改变后,scope是不会自己去执行每个watcher的listenerFn,必须要有个通知,而发送这个通知的就是 $digest

整个 $digest 的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirty check loop) 中, 循环后也有些次要的代码,如 postDigestQueue 的处理等就不作详细分析了。

脏值检查循环,意思就是说只要还有一个 watcher 的值存在更新那么就要运行一轮检查,直到没有值更新为止,当然为了减少不必要的检查作了一些优化。

代码:

);

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)
: (typeof value === 'number' && typeof last === '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就不用检查了
  else if (watch === lastDirtyWatch) {
   dirty = false;
   break traverseScopesLoop;
  }
 }
} 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 && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));

// break traverseScopesLoop 直接到这边

// 判断是不是还处在脏值循环中,并且已经超过最大检查次数 ttl默认10
if((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig','{0} $digest() iterations reached. AborTing!\n' +
'Watchers fired in the last 5 iterations: {1}',TTL,toJson(watchLog));
}

} while (dirty || asyncQueue.length); // 循环结束

// 标记退出digest循环
clearPhase();

上述代码中存在3层循环

第一层判断 dirty,如果有脏值那么继续循环

{

第二层判断 scope 是否遍历完毕,代码翻译了下,然还是绕但是能看懂

{

{ next = current.$$childHead; } else if (current !== target && current.$$nextSibling) { next = current.$$nextSibling; } while (!next && current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } while (current = next);

第三层循环scope的 watchers

s.length; while (length--) { try { watch = watchers[length];

// ... 省略

E) { clearPhase(); $exceptionHandler(E); } }

3. $evalAsync

$evalAsync用于延迟执行,源码如下:

{ if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { $browser.defer(function() { if ($rootScope.$$asyncQueue.length) { $rootScope.$digest(); } }); }

this.$$asyncQueue.push({scope: this,expression: expr});
}

通过判断是否已经有 dirty check 在运行,或者已经有人触发过$evalAsync

$browser.defer(function() {
//...
});

如果不是使用defer,那么

p) { queue.push({scope: this,expression: exp});

this.$digest();
}

scope.$evalAsync(fn1);
scope.$evalAsync(fn2);

// 这样的结果是
// $digest() > fn1 > $digest() > fn2
// 但是实际需要达到的效果:$digest() > fn1 > fn2

上节 $digest 中省略了了async 的内容,位于第一层循环中

{ 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 的原型继承的特点实现。

源码:

E) { var ChildScope,child;

if (isolatE) {
child = new Scope();
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 = new this.$$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;
}
return child;
}

代码还算清楚,主要的细节是哪些属性需要独立,哪些需要基础下来。

最重要的代码:

this.$$childScopeClass.prototype = this;

就这样实现了继承。

3. 事件机制

{ var namedListeners = this.$$listeners[name]; if (!namedListeners) { this.$$listeners[name] = namedListeners = []; } namedListeners.push(listener);

var current = this;
do {
if (!current.$$listenerCount[name]) {
current.$$listenerCount[name] = 0;
}
current.$$listenerCount[name]++;
} while ((current = current.$parent));

var self = this;
return function() {
namedListeners[indexOf(namedListeners,listener)] = null;
decrementListenerCount(self,1,Name);
};
}

跟 $wathc 类似,也是存放到数组 -- namedListeners。

还有不一样的地方就是该scope和所有parent都保存了一个事件的统计数,广播事件时有用,后续分析。

{ if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++; } while ((current = current.$parent));

$emit 是向上广播事件。源码:

{ var empty = [],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;
return event;
}

// emit是向上的传播方式
scope = scope.$parent;
} while (scopE);

event.currentScope = null;

return event;
}

3.3 $broadcast

$broadcast 是向内传播,即向child传播,源码:

{ var target = this,current = target,next = target,targetScope: target,listeners,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] && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
}

event.currentScope = null;
return event;
}

其他逻辑比较简单,就是在深度遍历的那段代码比较绕,其实跟digest中的一样,就是多了在路径上判断是否有监听,current.$$listenerCount[name],从上面$on的代码可知,只要路径上存在child有监听,那么该路径头也是有数字的,相反如果没有说明该路径上所有child都没有监听事件。

!== target && current.$$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

$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);

1); var changeDetected = 0; var changeDetector = $parse(obj,$watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0;

// 根据返回的changeDetected判断是否变化
function $watchCollectionInterceptor(_value) {
// ...
return changeDetected;
}

// 通过此方法调用真正的listener,作为代理
function $watchCollectionAction() {

}

return this.$watch(changeDetector,$watchCollectionAction);
}

主脉络就是上面截取的部分代码,下面主要分析 $watchCollectionInterceptor 和 $watchCollectionAction

value) { newValue = _value; var newLength,key,bothNaN,newItem,oldItem;

if (isUndefined(newvalue)) return;

if (!isObject(newvalue)) {
if (oldValue !== newvalue) {
oldValue = newValue;
changeDetected++;
}
} else if (isArrayLike(newvalue)) {
if (oldValue !== internalArray) {
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}

newLength = newValue.length;

if (oldLength !== newLength) {
changeDetected++;
oldValue.length = oldLength = newLength;
}
for (var i = 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 (key in newvalue) {
if (hasOwnProperty.call(newValue,key)) {
newLength++;
newItem = newValue[key];
oldItem = oldValue[key];

if (key in oldvalue) {
 bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
 if (!bothNaN && (oldItem !== newItem)) {
  changeDetected++;
  oldValue[key] = newItem;
 }
} else {
 oldLength++;
 oldValue[key] = newItem;
 changeDetected++;
}

}
}
if (oldLength > newLength) {
changeDetected++;
for (key in oldvalue) {
if (!hasOwnProperty.call(newValue,key)) {
oldLength--;
delete oldValue[key];
}
}
}
}
return changeDetected;
}

1). 当值为undefined时直接返回。

2). 当值为普通基本类型时 直接判断是否相等。

3). 当值为类数组 (即存在 length 属性,并且 value[i] 也成立称为类数组),先没有初始化先初始化oldValue

!== internalArray) { oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++; }

然后比较数组长度,不等的话记为已变化 changeDetected++

!== newLength) { changeDetected++; oldValue.length = oldLength = newLength; }

再进行逐个比较

{ oldItem = oldValue[i]; newItem = newValue[i];

bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[i] = newItem;
}
}

4). 当值为object时,类似上面进行初始化处理

!== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++; }

接下来的处理比较有技巧,但凡发现 newValue 多的新字段,就在oldLength 加1,这样 oldLength 只加不减,很容易发现 newValue 中是否有新字段出现,最后把 oldValue中多出来的字段也就是 newValue 中删除的字段给移除就结束了。

value) { if (hasOwnProperty.call(newValue,key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key];

if (key in oldvalue) {
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[key] = newItem;
}
} else {
oldLength++;
oldValue[key] = newItem;
changeDetected++;
}
}
}
if (oldLength > newLength) {
changeDetected++;
for (key in oldvalue) {
if (!hasOwnProperty.call(newValue,key)) {
oldLength--;
delete oldValue[key];
}
}
}

{ 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;
} else if (isArrayLike(newvalue)) {
veryOldValue = new Array(newValue.length);
for (var i = 0; i < newValue.length; i++) {
veryOldValue[i] = newValue[i];
}
} else {
veryOldValue = {};
for (var key in newvalue) {
if (hasOwnProperty.call(newValue,key)) {
veryOldValue[key] = newValue[key];
}
}
}
}
}

代码还是比较简单,就是调用 listenerFn,初次调用时 oldValue == newValue,为了效率和内存判断了下 listener是否需要oldValue参数

{ return $parse(expr)(this,locals); },$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (E) { $exceptionHandler(E); } finally { clearPhase(); try { $rootScope.$digest(); } catch (E) { $exceptionHandler(E); throw e; } } }

$apply 最后调用 $rootScope.$digest(),所以很多书上建议使用 $digest() ,而不是调用 $apply(),效率要高点。

主要逻辑都在$parse 属于语法解析功能,后续单独分析。

大佬总结

以上是大佬教程为你收集整理的angularjs 源码解析之scope全部内容,希望文章能够帮你解决angularjs 源码解析之scope所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。