2019独角兽企业重金招聘Python工程师标准>>>

简介

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

监听

1. $watch

1.1 使用

// $watch: function(watchExp, listener, objectEquality)var unwatch = $scope.$watch('aa', function () {}, isEqual);

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

1.2 源码分析

function(watchExp, listener, objectEquality) {var scope = this,// 将可能的字符串编译成fnget = 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

2.1 源码分析

整个 $digest 的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirty check loop) 中, 循环后也有些次要的代码,如 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 !== NaNif ((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 也设置为newValuewatch.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默认10if((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,如果有脏值那么继续循环

do {// ...} while (dirty)

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

do {// ....if (current.$$childHead) {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

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

通过判断是否已经有 dirty check 在运行,或者已经有人触发过$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) {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. 事件机制

3.1 $on

function(name, listener) {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都保存了一个事件的统计数,广播事件时有用,后续分析。

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

3.2 $emit

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

function(name, args) {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);}}// 停止传播时returnif (stopPropagation) {event.currentScope = null;return event;}// emit是向上的传播方式scope = scope.$parent;} while (scope);event.currentScope = null;return event;
}

3.3 $broadcast

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

function(name, args) {var target = 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] && 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都没有监听事件。

if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||(current !== 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

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;var self = this;var newValue;var oldValue;var veryOldValue;var trackVeryOldValue = (listener.length > 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

4.3 $watchCollectionInterceptor

function $watchCollectionInterceptor(_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

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

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

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;}
}

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

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

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

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];}}
}

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;} 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参数

5. $eval & $apply

$eval: function(expr, locals) {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 属于语法解析功能,后续单独分析。

转载于:https://my.oschina.net/alexqdjay/blog/734861

angularjs源码笔记(4)--scope相关推荐

  1. angularjs源码笔记(3)--injector

    2019独角兽企业重金招聘Python工程师标准>>> 简介 injector是用来做参数自动注入的,例如 function fn ($http, $scope, aService) ...

  2. spring aop原理_Spring知识点总结!已整理成142页离线文档(源码笔记+思维导图)...

    写在前面 由于Spring家族的东西很多,一次性写完也不太现实.所以这一次先更新Spring[最核心]的知识点:AOP和IOC 无论是入门还是面试,理解AOP和IOC都是非常重要的.在面试的时候,我没 ...

  3. 数据结构源码笔记(C语言描述)汇总

    数据结构源码笔记(C语言):英文单词按字典序排序的基数排序 数据结构源码笔记(C语言):直接插入排序 数据结构源码笔记(C语言):直接选择排序 数据结构源码笔记(C语言):置换-选择算法 数据结构源码 ...

  4. 数据结构源码笔记(C语言):英文单词按字典序排序的基数排序

    //实现英文单词按字典序排序的基数排序算法#include<stdio.h> #include<malloc.h> #include<string.h>#defin ...

  5. 数据结构源码笔记(C语言):索引文件建立和查找

    //实现索引文件建立和查找算法#include<stdio.h> #include<malloc.h> #include<string.h> #include< ...

  6. 数据结构源码笔记(C语言):快速排序

    //实现快速排序算法 #include<stdio.h> #include<malloc.h> #define MAXE 20typedef int KeyType; type ...

  7. 数据结构源码笔记(C语言):冒泡排序

    //冒泡排序算法实现 #include<stdio.h> #include<malloc.h> #define MAXE 20typedef int KeyType; type ...

  8. 数据结构源码笔记(C语言):希尔插入排序

    //实现希尔插入排序算法 #include<stdio.h> #include<malloc.h> #define MAXE 20typedef int KeyType; ty ...

  9. 数据结构源码笔记(C语言):直接插入排序

    //实现直接插入排序算法#include<stdio.h> #include<malloc.h> #define MAXE 20typedef int KeyType; typ ...

最新文章

  1. mysql isolation level_MySQL数据库事务隔离级别(Transaction Isolation Level)
  2. java多线程------实现Runnable接口创建多进程,实现资源共享
  3. HTML中行内元素与块级元素的区别
  4. HDU - 3694 Fermat Point in Quadrangle(三分套三分/凸包)
  5. 零基础自学编程前需要知道的知识
  6. STL源码剖析 hashtable
  7. mysql如何查看表拥有的键_如何查看表或列的所有外键?
  8. Centos7 安装samba简单教程
  9. 数据库:跨数据库,服务器数据迁移
  10. 11. 瞬时响应:网站的高性能架构
  11. ISO9001\ISO14001\OHSAS18001三体系快速认证申报须知
  12. 磁力计校正和数据处理
  13. ZT——你怎么过河? -在CMM实践中你是否愿意多走1公里-软件工程 CMM与过程改进
  14. stack在python中是什么意思_python栈是什么意思
  15. [机器翻译]—BLEU值的计算
  16. 校验组织机构代码 合法性
  17. pboc2.0证书解析
  18. IObit Uninstaller安装
  19. Asp.Net常见问题及技术实现方案(一)
  20. 恶人自有天收:如何能使僵尸网络Mirai的服务器宕机

热门文章

  1. 【转载】ubuntu换源
  2. 高中选的美术将来能考计算机学校吗,北京中考美术考上美术高中以后上考大学一定要考美术专业的大学?好考美术高中...
  3. day17--JQuery
  4. Spacecom:将和信威集团在30天内决定新的收购协议
  5. Python yield generator
  6. MaxCompute实战之数据存储
  7. STL 源代码分析 算法 stl_algo.h -- includes
  8. 总结 15/4/23
  9. 怎样用调用资源管理器explore.exe打开指定的文件夹
  10. 8-2 主从复制高可用