angularjs 源码解析之scope


Posted in Javascript onAugust 22, 2016

简介

在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,
   // 将可能的字符串编译成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

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 !== 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,如果有脏值那么继续循环

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);
   }
  }
  // 停止传播时return
  if (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 属于语法解析功能,后续单独分析。

Javascript 相关文章推荐
js用图作提交按钮或超连接
Mar 26 Javascript
从零开始学习jQuery (六) jquery中的AJAX使用
Feb 23 Javascript
Jquery时间验证和转换工具小例子
Jul 01 Javascript
jquery插件开发之实现google+圈子选择功能
Mar 10 Javascript
Jquery ajax加载等待执行结束再继续执行下面代码操作
Nov 24 Javascript
jQuery取得元素标签名称小结(附代码)
Aug 16 jQuery
Vue利用路由钩子token过期后跳转到登录页的实例
Oct 26 Javascript
JS从非数组对象转数组的方法小结
Mar 26 Javascript
VeeValidate在vue项目里表单校验应用案例
May 09 Javascript
vue实现文字横向无缝走马灯组件效果的实例代码
Apr 09 Javascript
微信小程序动态添加和删除组件的现实
Feb 28 Javascript
jquery插件懒加载的示例
Oct 24 jQuery
js表单元素checked、radio被选中的几种方法(详解)
Aug 22 #Javascript
js严格模式总结(分享)
Aug 22 #Javascript
xtemplate node.js 的使用方法实例解析
Aug 22 #Javascript
node.js express安装及示例网站搭建方法(分享)
Aug 22 #Javascript
angularjs 源码解析之injector
Aug 22 #Javascript
基于jQuery实现表格内容的筛选功能
Aug 21 #Javascript
jQuery Easyui快速入门教程
Aug 21 #Javascript
You might like
解析Linux下Varnish缓存的配置优化
2013/06/20 PHP
PHP回调函数概念与用法实例分析
2017/11/03 PHP
利用PHP扩展Xhprof分析项目性能实践教程
2018/09/05 PHP
Flash+XML滚动新闻代码 无图片 附源码下载
2007/11/22 Javascript
ExtJS 2.0实用简明教程 之ExtJS版的Hello
2009/04/29 Javascript
jQuery帮助之CSS尺寸(五)outerHeight、outerWidth
2009/11/14 Javascript
javascript 放大镜 v1.0 基于Yui2 实现的放大镜效果
2010/03/08 Javascript
用于节点操作的API,颠覆原生操作HTML DOM节点的API
2010/12/11 Javascript
web的各种前端打印方法之jquery打印插件jqprint实现网页打印
2013/01/09 Javascript
javascript异步处理工作机制详解
2015/04/13 Javascript
jQuery插件Tmpl的简单使用方法
2015/04/27 Javascript
jquery实现多屏多图焦点图切换特效的方法
2015/05/04 Javascript
浅析在javascript中创建对象的各种模式
2016/05/06 Javascript
JS中使用变量保存arguments对象的方法
2016/06/03 Javascript
Node.js连接MongoDB数据库产生的问题
2017/02/08 Javascript
jQuery Ajax使用FormData上传文件和其他数据后端web.py获取
2017/06/11 jQuery
jQuery实现的文字逐行向上间歇滚动效果示例
2017/09/06 jQuery
vue实现鼠标经过动画
2019/10/16 Javascript
vue+elementui 对话框取消 表单验证重置示例
2019/10/29 Javascript
Python使用迭代器捕获Generator返回值的方法
2017/04/05 Python
python3读取excel文件只提取某些行某些列的值方法
2018/07/10 Python
对python3新增的byte类型详解
2018/12/04 Python
python给视频添加背景音乐并改变音量的具体方法
2020/07/19 Python
如何使用python-opencv批量生成带噪点噪线的数字验证码
2020/12/21 Python
Html5页面内使用JSON动画的实现
2019/01/29 HTML / CSS
澳大利亚礼品卡商店:Gift Card Store
2019/06/24 全球购物
.NET remoting的两种通道是什么
2016/05/31 面试题
人力资源经理自我评价
2014/01/04 职场文书
《雷雨》教学反思
2014/02/20 职场文书
十佳青年事迹材料
2014/08/21 职场文书
学生保证书格式
2015/02/27 职场文书
2015年社会实践个人总结
2015/03/06 职场文书
运动会表扬稿范文
2015/05/05 职场文书
青年岗位能手事迹材料(2016推荐版)
2016/03/01 职场文书
Python几种酷炫的进度条的方式
2022/04/11 Python
Spring Data JPA框架的核心概念和Repository接口
2022/04/28 Java/Android