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 相关文章推荐
ExtJS 2.0实用简明教程 之ExtJS版的Hello
Apr 29 Javascript
利用javascript判断文件是否存在
Dec 31 Javascript
jquery复选框checkbox实现删除前判断
Apr 20 Javascript
javascript解析json数据的3种方式
May 08 Javascript
jQuery中JSONP的两种实现方式详解
Sep 26 Javascript
JavaScript中双符号的运算详解
Mar 12 Javascript
ES6学习笔记之Set和Map数据结构详解
Apr 07 Javascript
xmlplus组件设计系列之树(Tree)(9)
May 02 Javascript
vue router自动判断左右翻页转场动画效果
Oct 10 Javascript
纯js代码生成可搜索选择下拉列表的实例
Jan 11 Javascript
JavaScript实现前端网页版倒计时
Mar 24 Javascript
vue项目两种方式实现竖向表格的思路分析
Apr 28 Vue.js
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
php实现的在线人员函数库
2008/04/09 PHP
PHP sprintf() 函数的应用(定义和用法)
2012/06/29 PHP
ThinkPHP使用smarty模板引擎的方法
2014/07/01 PHP
MyEclipse常用配置图文教程
2014/09/11 PHP
php技术实现加载字体并保存成图片
2015/07/27 PHP
yii2 RBAC使用DbManager实现后台权限判断的方法
2016/07/23 PHP
Yii2 队列 shmilyzxt/yii2-queue 简单概述
2017/08/02 PHP
基于Laravel5.4实现多字段登录功能方法示例
2017/08/11 PHP
学习YUI.Ext 第四天--对话框Dialog的使用
2007/03/10 Javascript
分享Javascript中最常用的55个经典小技巧
2013/11/29 Javascript
javascript中定义私有方法说明(private method)
2014/01/27 Javascript
SeaJS入门教程系列之使用SeaJS(二)
2014/03/03 Javascript
js通过更改按钮的显示样式实现按钮的滑动效果
2014/04/23 Javascript
js闭包实例汇总
2014/11/09 Javascript
用模版生成HTML的的框架jquery.tmpl使用详解
2015/01/07 Javascript
jquery背景跟随鼠标滑动导航
2015/11/20 Javascript
React-router中结合webpack实现按需加载实例
2017/05/25 Javascript
浅谈在Vue-cli里基于axios封装复用请求
2017/11/06 Javascript
vue路由拦截及页面跳转的设置方法
2018/05/24 Javascript
JS获取当前时间的年月日时分秒及时间的格式化的方法
2019/12/18 Javascript
JS如何寻找数组中心索引过程解析
2020/06/01 Javascript
nodejs中的异步编程知识点详解
2021/01/17 NodeJs
Python 获取当前所在目录的方法详解
2017/08/02 Python
详解Django-channels 实现WebSocket实例
2019/08/22 Python
基于Django框架的权限组件rbac实例讲解
2019/08/31 Python
python面向对象之类属性和类方法案例分析
2019/12/30 Python
python下载的库包存放路径
2020/07/27 Python
python实现图片转换成素描和漫画格式
2020/08/19 Python
css3隔行变换色实现示例
2014/02/19 HTML / CSS
群众路线自查自纠工作情况报告
2014/10/28 职场文书
工资证明格式模板
2015/06/12 职场文书
医疗纠纷调解协议书
2015/08/06 职场文书
教您:房贷工资收入证明应该怎么写?
2019/08/19 职场文书
python模块与C和C++动态库相互调用实现过程示例
2021/11/02 Python
使用Python获取字典键对应值的方法
2022/04/26 Python
Java实现注册登录跳转
2022/06/16 Java/Android