发表于: 2017-03-10 16:22:06

1 847


AngularJS1.x实现原理——scope部分

scope有多种用途: 

- 在controller和view之间共享数据 

- 在app的不同部分之间共享数据 

- 广播和监听事件 

- 监控数据变化

这里主要讲4个方面的第一部分

- digest和脏检查,包含$watch, $digest, $apply 

- scope的继承,创建scope层次结构用于共享数据和事件的机制 

- 脏检查对于数组和对象的效率 

- $on, $emit, $broadcast的事件系统

scope和digest

首先把scope当做一个实例化的对象

在全局上可以用这样的方式实现:

  1. var scope = new Scope();

监视对象的属性用$watch 和 $digest

$watch 和 $digest就像一枚硬币的正反面,放在一起的主要作用就是为循环检查数据变化。 用$watch让watcher和scope产生关系,一个watcher会在scope里的数据发生改变时给出提示。如果我们来创造可以给监视器提供两个函数来实现这个功能:1个是watch函数监视数据,2个是listener函数,在数据发生改变时会调用它。 而$digest则是硬币的另外一面,遍历scope所有的watchers,调用它们的watch和listener函数。 总的说来就是,要注册一个watcher的时候就调用$watch,而它的listener函数则在有调用$digest时被调用。

  1. // 创建scope构造函数 
  2. function Scope() { 
  3.   this.$$watchers = []; //双$$在angular里表示框架私有 
  4. // 给scope原型添加$watch方法,接收2个参数,并且将它们保存到$$watchers数组里 
  5. Scope.prototype.$watch = function(watchFn, listenerFn) { 
  6.   var watcher = { 
  7.     watchFn: watchFn, 
  8.     listenerFn: listenerFn 
  9.   }; 
  10.   this.$$watchers.push(watcher); 
  11. }; 
  12. // 最后添加$digest函数,首先定义一个极简版本,仅仅只是遍历scope的watchers,然后调用它们自己的listener函数 
  13. Scope.prototype.$digest = function() { 
  14.   _.forEach(this.$$watchers, function(watcher) { 
  15.     watcher.listenerFn(); 
  16.   }) 
  17. }; 
  18. // 这个版本的$digest还没完善,我们真正需要的是数值通过watch函数发生变化后调用listener函数,那才是真正的脏检查(dirty-checking),不过可以先看着理解一下

脏检查

综上所述,watch函数应该是返回scope作用域下的一段数据,作为监视用。比如这个watch对scope的firstName属性感兴趣,那么函数应该就是如下:

  1. function(scope) { 
  2.  return scope.firstName; 
  3. }

这样就需要再改造一下$digest,将scope传入到watch函数中:

  1. Scope.prototype.$digest = function() { 
  2.   var self = this
  3.   _.forEach(this.$$watchers, function(watcher) { 
  4.     watcher.watchFn(self); 
  5.     watcher.listenerFn(self); 
  6.   }) 
  7. }

实际运用一下:

  1. // 初始化一个scope 
  2. var scope = new Scope(); 
  3. // 给scope添加属性 
  4. scope.first = 'a'
  5. scope.counter = 0
  6. // 准备监视first这个属性 
  7. var watchFirst = function(scope) {return scope.first;}; 
  8. // 简化的listener,每执行一次给counter增1 
  9. var listenerFirst = function(newVal, oldVal, scope) {scope.counter++;}; 
  10. // 给scope挂上$watch方法,目前scope.counter == 0; 
  11. scope.$watch(watchFirst, listenerFirst); 
  12.  
  13. // 执行digest 
  14. scope.$digest(); // scope.counter == 1 
  15. scope.$digest(); // scope.counter == 2 
  16. scope.$digest(); // scope.counter == 3

上面的做法是实现了循环,但每次都执行了listener函数,并没打成只对脏数据进行检查这个目的,因此我们再来修改一下digest。 增加一个last用于存储上一次的值,让它每次循环的时候存储watch的旧值然后和新值进行对比,如果对比不一致就执行listener,否则不执行。

  1. Scope.prototype.$watch = function(watchFn, listenerFn) { 
  2.   var watcher = { 
  3.     watchFn: watchFn, 
  4.     listenerFn: listenerFn 
  5.   }; 
  6.   this.$$watchers.push(watcher); 
  7. }; 
  8. Scope.prototype.$digest = function() { 
  9.   var self = this
  10.   var newValue, oldValue; 
  11.   _.forEach(this.$$watchers, function(watcher) { 
  12.    newValue = watcher.watchFn(self); 
  13.    oldValue = watcher.last; 
  14.    if (newValue != oldValue) { 
  15.     watcher.last = newValue; 
  16.     watcher.listenerFn(newValue, oldValue, self); 
  17.    } 
  18.   }) 
  19. };

初始化$watch的值

刚刚的做法虽然有个last保存上一次的值,但还没给last赋值之前,它的值将是undefined,因此我们考虑将last在初始化时作为属性赋给watcher

  1. // 设置一个initWatchVal()函数以供以后last初始化使用 
  2. function initWatchVal() {} 
  3.  
  4. Scope.prototype.$watch = function(watchFn, listenerFn) { 
  5.   var watcher = { 
  6.     watchFn: watchFn, 
  7.     listenerFn: listenerFn, 
  8.     last: initWatchVal 
  9.   }; 
  10.   this.$$watchers.push(watcher) 
  11. Scope.prototype.$digest = function() { 
  12.   var self = this
  13.   var newValue, oldValue; 
  14.   _.forEach(this.$$watchers, function(watcher) { 
  15.    newValue = watcher.watchFn(self); 
  16.    oldValue = watcher.last; 
  17.    if (newValue != oldValue) { 
  18.     watcher.last = newValue; 
  19.     // listenerFn的oldvalue也做一点调整,检查oldValue是否和initValue一样,一样的话还是让它等于newValue,因为这表示它是第一次调用,没有数据还未被污染 
  20.     watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self); 
  21.    } 
  22.   }) 
  23. };

有脏数据则保持循环

上面已经完成了部分核心功能,但还不够完善。当有多个监视数据时,有可能出现这样的情况:

  1. var scope = new Scope(); 
  2. scope.name = 'Jane'
  3. // 监视数据scope.nameUpper 
  4. scope.$watch( 
  5.   function(scope) {return scope.nameUpper}, 
  6.   function(newValue, oldValue, scope) { 
  7.     if (newValue) { 
  8.       scope.first = newValue.substring(01); 
  9.     } 
  10.   } 
  11. ); 
  12. // 监视数据scope.name 
  13. scope.$watch( 
  14.   function(scope) {return scope.name}, 
  15.   function(newValue, oldValue, scope) { 
  16.     if (newValue) { 
  17.       scope.nameUpper = newValue.toUpperCase(); 
  18.     } 
  19.   } 
  20. scope.$digest(); // scope.first 期望值是'J', 实际是空 
  21. scope.name = 'Bob'
  22. scope.$digest(); // scope.first 期望值是'B', 实际是'B'

以上的代码,监视了两个scope的两个数据,但由于推入的顺序问题,导致第一次执行循环时,先执行到scope.nameUpper是undefined因此无法获得scope.first的值。这样明显是有问题的。我们希望的是,无论先后加入监视,都能在最后的listener中正确执行。因此我们这样改造一下代码,对数据进行循环检查,保证不会因为undefined而出现这样的情况:

  1. Scope.$$digestOnce = function() { 
  2.   var self = this
  3.   var newValue, oldValue, dirty; 
  4.   _.forEach(this.$$watchers, function(watcher) { 
  5.     newValue = watcher.watchFn(self); 
  6.     oldValue = watcher.last; 
  7.     if (newValue != oldValue) { 
  8.       watcher.last = newValue; 
  9.       watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self); 
  10.       dirty = true
  11.     } 
  12.   }); 
  13.   return dirty; 
  14. }; 
  15. Scope.$digest = function() { 
  16.   var dirty; 
  17.   do { 
  18.     dirty = this.$$digestOnce(); 
  19.   } while (dirty); 
  20. };

这样在listener的新老值不一样时,会使得dirty为真,导致再执行一次$$digestOnce。一直要到循环到新老值完全一样时才跳出循环。 这样再来执行刚刚的两次$digest()代码,会发现,第一个$digest在执行时,第一次进入$$digestOnce时执行了两个监视数据的listenerFn(scope.first为空,scope.nameUpper=='JANE'),此时dirty为true,导致这里执行第二个$$digestOnce,而这次由于上次循环时已经获得了scope.nameUpper的值,所以此时便可以获得scope.first的值为'J'了。直到新老值完全一样后,dirty不再为true,便可以退出循环。

终止不稳定的循环

按上面的做法,虽然是能通过多次循环来达到同步数据的目的,但是如果多个数据之间出现相互调用,那么可能让循环进入恶性循环,无法终止。因此这里再给$digest增加一条次数上的限制:

  1. Scope.prototype.$digest = function() { 
  2.   var ttl = 10
  3.   var dirty; 
  4.   do { 
  5.     dirty = this.$$digestOnce(); 
  6.     if (dirty && !(ttl--)) { 
  7.       throw "超出10次循环限制了!" 
  8.     } 
  9.   } while (dirty) 
  10. }

NaN

在JS里,NaN是不等于它自身的。因此在angular的scope里,如果一个scope的属性是NaN,那么它是不会被加入watch的循环的。因为如果监视了它,会发现做脏检查时它始终都为脏,newValue和oldValue的值始终都不会一样。 这里可以在scope的原型上增加一个判断值的方法

  1. Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 
  2.   if (valueEq) { 
  3.     return _.isEqual(newValue, oldValue); 
  4.   } else { 
  5.     return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); 
  6.   } 
  7. }

$apply —— 整合digest循环

scope里最有名的方法之一大概就是$apply了,它被当做是调用digest的外部标准库。

  1. // $eval用于在scope的上下文中执行一段代码expr 
  2. Scope.prototype.$eval = function(expr, locals) { 
  3.   return expr(this, locals) 
  4. }; 
  5. Scope.prototype.$apply = function(expr) { 
  6.   try { 
  7.     return this.$eval(expr); 
  8.   } finally { 
  9.     this.$digest(); 
  10.   } 
  11.  
  12. // 实际使用中,保证代码的执行和加入digest监视循环 
  13. scope.$apply(function(scope) { 
  14.   scope.val = 'something'
  15. });

到这里基本的原理就是这样的了,当然实际上的代码会比这个复杂很多,功能也要完善得多。这里只讲了最核心的一点功能的思路,如果对这个有兴趣的朋友还是直接看源码更佳。



返回列表 返回列表
评论

    分享到