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 相关文章推荐
权威JavaScript 中的内存泄露模式
Aug 13 Javascript
checkbox 复选框不能为空
Jul 11 Javascript
jQuery操作checkbox选择(list/table)
Apr 07 Javascript
jQuery 滑动方法slideDown向下滑动元素
Jan 16 Javascript
JavaScript判断表单提交时哪个radio按钮被选中的方法
Mar 21 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
May 12 Javascript
js改变css样式的三种方法推荐
Jun 28 Javascript
KnockoutJS 3.X API 第四章之表单submit、enable、disable绑定
Oct 10 Javascript
ES5 模拟 ES6 的 Symbol 实现私有成员功能示例
May 06 Javascript
jQuery实现移动端下拉展现新的内容回弹动画
Jun 24 jQuery
解决antd日期选择组件,添加value就无法点击下一年和下一月问题
Oct 29 Javascript
浅谈react useEffect闭包的坑
Jun 08 Javascript
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
WINDOWS 2000下使用ISAPI方式安装PHP
2006/09/05 PHP
Apache2 httpd.conf 中文版
2006/12/06 PHP
PHP实现简单的新闻发布系统实例
2015/07/28 PHP
微信自定义分享php代码分析
2016/11/24 PHP
通过js脚本复制网页上的一个表格的不错实现方法
2006/12/29 Javascript
动态控制Table的js代码
2007/03/07 Javascript
JS获取select-option-text_value的方法
2013/12/26 Javascript
js/jQuery简单实现选项卡功能
2014/01/02 Javascript
js触发select onchange事件的小技巧
2014/08/05 Javascript
javascript 自定义回调函数示例代码
2014/09/26 Javascript
html的DOM中Event对象onblur事件用法实例
2015/01/21 Javascript
js+html5实现canvas绘制镂空字体文本的方法
2015/06/05 Javascript
js漂浮广告实现代码
2015/08/15 Javascript
浅析创建javascript对象的方法
2016/05/13 Javascript
垃圾回收器的相关知识点总结
2018/05/13 Javascript
vue使用video.js进行视频播放功能
2019/07/18 Javascript
详解json串反转义(消除反斜杠)
2019/08/12 Javascript
浅析 Vue 3.0 的组装式 API(一)
2020/08/31 Javascript
微信小程序实现分页加载效果
2020/11/19 Javascript
Django小白教程之Django用户注册与登录
2016/04/22 Python
Python、PyCharm安装及使用方法(Mac版)详解
2017/04/28 Python
django限制匿名用户访问及重定向的方法实例
2018/02/07 Python
tensorflow 使用flags定义命令行参数的方法
2018/04/23 Python
python中的&amp;&amp;及||的实现示例
2019/08/07 Python
CSS3中:nth-child和:nth-of-type的区别深入理解
2014/03/10 HTML / CSS
html5中使用hotcss.js实现手机端自适配的方法
2020/04/23 HTML / CSS
美国性感女装网站:bebe
2017/03/04 全球购物
彪马西班牙官网:PUMA西班牙
2019/06/18 全球购物
台湾良兴购物网:EcLife
2019/12/01 全球购物
static全局变量与普通的全局变量有什么区别
2014/05/27 面试题
会计专业应届生自荐信
2014/02/07 职场文书
《手指教学》反思
2014/02/14 职场文书
领导批评与自我批评范文
2014/10/16 职场文书
2016三八妇女节慰问信
2015/11/30 职场文书
员工工作心得体会
2019/05/07 职场文书
微软PC Health Check电脑健康状况检查应用下载(Win11配置检测工具)
2021/06/26 数码科技