欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

angularjs 源码解析之scope

程序员文章站 2023-11-14 15:50:52
简介 在ng的生态中scope处于一个核心的地位,ng对外宣称的双向绑定的底层其实就是scope实现的,本章主要对scope的watch机制、继承性以及事件的实现作下分析...

简介

在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 属于语法解析功能,后续单独分析。