前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >angularjs源码笔记(4)--scope

angularjs源码笔记(4)--scope

作者头像
alexqdjay
发布2022-01-04 16:58:37
1.2K0
发布2022-01-04 16:58:37
举报
文章被收录于专栏:alexqdjayalexqdjay

简介

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

监听

1. $watch

1.1 使用

代码语言:javascript
复制
// $watch: function(watchExp, listener, objectEquality)

var unwatch = $scope.$watch('aa', function () {}, isEqual);

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

1.2 源码分析

代码语言:javascript
复制
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 保存到

2. $digest

当 scope 的值发生改变后,scope是不会自己去执行每个watcher的listenerFn,必须要有个通知,而发送这个通知的就是 $digest

2.1 源码分析

整个 $digest 的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirty check loop) 中, 循环后也有些次要的代码,如 postDigestQueue 的处理等就不作详细分析了。

脏值检查循环,意思就是说只要还有一个 watcher 的值存在更新那么就要运行一轮检查,直到没有值更新为止,当然为了减少不必要的检查作了一些优化。

代码:

代码语言:javascript
复制
// 进入$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,如果有脏值那么继续循环

代码语言:javascript
复制
do {

  // ...

} while (dirty)

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

代码语言:javascript
复制
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

代码语言:javascript
复制
length = watchers.length;
while (length--) {
  try {
    watch = watchers[length];
    
    // ... 省略

  } catch (e) {
    clearPhase();
    $exceptionHandler(e);
  }
}

3. $evalAsync

3.1 源码分析

$evalAsync用于延迟执行,源码如下:

代码语言:javascript
复制
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

代码语言:javascript
复制
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)

$browser.defer 就是通过调用 setTimeout 来达到改变执行顺序

代码语言:javascript
复制
$browser.defer(function() {
  //...     
});

如果不是使用defer,那么

代码语言:javascript
复制
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 的内容,位于第一层循环中

代码语言:javascript
复制
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。

代码语言:javascript
复制
lastDirtyWatch = null;

还有这边还有一个细节,为什么在第一层循环呢?因为具有继承关系的scope其 $$asyncQueue 是公用的,都是挂载在root上,故不需要在下一层的scope层中执行。

2. 继承性

scope具有继承性,如 parentScope, childScope 两个scope,当调用 childScope.fn 时如果 childScope 中没有 fn 这个方法,那么就是去

这样一层层往上查找直到找到需要的属性。这个特性是利用 javascirpt 的原型继承的特点实现。

源码:

代码语言:javascript
复制
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;
}

代码还算清楚,主要的细节是哪些属性需要独立,哪些需要基础下来。

最重要的代码:

代码语言:javascript
复制
this.$$childScopeClass.prototype = this;

就这样实现了继承。

3. 事件机制

3.1 $on

代码语言:javascript
复制
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都保存了一个事件的统计数,广播事件时有用,后续分析。

代码语言:javascript
复制
var current = this;
do {
  if (!current.$$listenerCount[name]) {
    current.$$listenerCount[name] = 0;
  }
  current.$$listenerCount[name]++;
} while ((current = current.$parent));

3.2 $emit

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

代码语言:javascript
复制
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传播,源码:

代码语言:javascript
复制
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都没有监听事件。

代码语言:javascript
复制
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
        (current !== target && current.$$nextSibling)))) {
  while(current !== target && !(next = current.$$nextSibling)) {
    current = current.$parent;
  }
}

传播路径:

代码语言:javascript
复制
Root>[A>[a1,a2], B>[b1,b2>[c1,c2],b3]]

Root > A > a1 > a2 > B > b1 > b2 > c1 > c2 > b3

4. $watchCollection

4.1 使用示例

代码语言:javascript
复制
$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 源码分析

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
if (oldValue !== internalArray) {
  oldValue = internalArray;
  oldLength = oldValue.length = 0;
  changeDetected++;
}

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

代码语言:javascript
复制
if (oldLength !== newLength) {
  changeDetected++;
  oldValue.length = oldLength = newLength;
}

再进行逐个比较

代码语言:javascript
复制
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时,类似上面进行初始化处理

代码语言:javascript
复制
if (oldValue !== internalObject) {
  oldValue = internalObject = {};
  oldLength = 0;
  changeDetected++;
}

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
$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() ,而不是调用

主要逻辑都在$parse 属于语法解析功能,后续单独分析。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016/08/18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 监听
    • 1. $watch
      • 1.1 使用
      • 1.2 源码分析
    • 2. $digest
      • 2.1 源码分析
    • 3. $evalAsync
      • 3.1 源码分析
    • 2. 继承性
      • 3. 事件机制
        • 3.1 $on
        • 3.2 $emit
        • 3.3 $broadcast
      • 4. $watchCollection
        • 4.1 使用示例
        • 4.2 源码分析
        • 4.3 $watchCollectionInterceptor
        • 4.4 $watchCollectionAction
      • 5. $eval & $apply
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档