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 相关文章推荐
FireFox JavaScript全局Event对象
Jun 14 Javascript
javascript 特性检测并非浏览器检测
Jan 15 Javascript
获取dom元素那些讨厌的位置封装代码
Jun 23 Javascript
jquery的ajax跨域请求原理和示例
May 08 Javascript
jQuery常用操作方法及常用函数总结
Jun 19 Javascript
jQuery 重复加载错误以及修复方法
Dec 16 Javascript
canvas绘制环形进度条
Feb 23 Javascript
理解 javascript 中的函数表达式与函数声明
Jul 07 Javascript
Node.js中sequelize时区的配置方法
Dec 10 Javascript
vue 组件开发原理与实现方法详解
Nov 29 Javascript
vue3.0实现插件封装
Dec 14 Vue.js
Node.js实现断点续传
Jun 23 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
实时抓取YAHOO股票报价的代码
2006/10/09 PHP
优化使用mysql存储session的php代码
2008/01/10 PHP
php采集速度探究总结(原创)
2008/04/18 PHP
php下mysql数据库操作类(改自discuz)
2010/07/03 PHP
PHP+Mysql+jQuery实现动态展示信息
2011/10/08 PHP
PHP微信开发之模板消息回复
2016/06/24 PHP
js FLASH幻灯片字符串中有连接符&的处理方法
2012/03/01 Javascript
javascript中节点的最近的相关节点访问方法
2013/03/20 Javascript
javascript的内存管理详解
2013/08/07 Javascript
javascript模块化是什么及其优缺点介绍
2013/09/02 Javascript
Jquery简单分页实现方法
2015/07/24 Javascript
JavaScript 封装一个tab效果源码分享
2015/09/15 Javascript
DIV随滚动条滚动而滚动的实现代码【推荐】
2016/04/12 Javascript
Javascript从数组中随机取出不同元素的两种方法
2016/09/22 Javascript
详解webpack异步加载业务模块
2017/06/23 Javascript
Echarts基本用法_动力节点Java学院整理
2017/08/11 Javascript
详解关于JSON.parse()和JSON.stringify()的性能小测试
2019/03/14 Javascript
JS利用prototype给类添加方法操作详解
2019/06/21 Javascript
Vue初始化中的选项合并之initInternalComponent详解
2020/06/11 Javascript
Javascript基于OOP实实现探测器功能代码实例
2020/08/26 Javascript
Python3数字求和的实例
2019/02/19 Python
Python学习笔记之Django创建第一个数据库模型的方法
2019/08/07 Python
使用python turtle画高达
2020/01/19 Python
pytorch数据预处理错误的解决
2020/02/20 Python
一款简洁的纯css3代码实现的动画导航
2014/10/31 HTML / CSS
使用canvas生成含有微信头像的邀请海报没有微信头像问题
2019/10/29 HTML / CSS
厂长助理岗位职责
2013/12/27 职场文书
幼儿园教师教学反思
2014/02/06 职场文书
行政工作个人的自我评价
2014/02/13 职场文书
《小白兔和小灰兔》教学反思
2014/02/18 职场文书
市场营销专业毕业生求职信
2014/07/21 职场文书
个人批评与自我批评发言稿
2014/09/28 职场文书
事业单位个人总结
2015/02/12 职场文书
通知函格式范文
2015/04/27 职场文书
工作一年自我鉴定
2019/06/20 职场文书
Java org.w3c.dom.Document 类方法引用报错
2021/08/07 Java/Android