发表于: 2017-03-10 16:22:06
1 846
AngularJS1.x实现原理——scope部分
scope有多种用途:
- 在controller和view之间共享数据
- 在app的不同部分之间共享数据
- 广播和监听事件
- 监控数据变化
这里主要讲4个方面的第一部分
- digest和脏检查,包含$watch, $digest, $apply
- scope的继承,创建scope层次结构用于共享数据和事件的机制
- 脏检查对于数组和对象的效率
- $on, $emit, $broadcast的事件系统
scope和digest
首先把scope当做一个实例化的对象
在全局上可以用这样的方式实现:
- 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时被调用。
- // 创建scope构造函数
- function Scope() {
- this.$$watchers = []; //双$$在angular里表示框架私有
- }
- // 给scope原型添加$watch方法,接收2个参数,并且将它们保存到$$watchers数组里
- Scope.prototype.$watch = function(watchFn, listenerFn) {
- var watcher = {
- watchFn: watchFn,
- listenerFn: listenerFn
- };
- this.$$watchers.push(watcher);
- };
- // 最后添加$digest函数,首先定义一个极简版本,仅仅只是遍历scope的watchers,然后调用它们自己的listener函数
- Scope.prototype.$digest = function() {
- _.forEach(this.$$watchers, function(watcher) {
- watcher.listenerFn();
- })
- };
- // 这个版本的$digest还没完善,我们真正需要的是数值通过watch函数发生变化后调用listener函数,那才是真正的脏检查(dirty-checking),不过可以先看着理解一下
脏检查
综上所述,watch函数应该是返回scope作用域下的一段数据,作为监视用。比如这个watch对scope的firstName属性感兴趣,那么函数应该就是如下:
- function(scope) {
- return scope.firstName;
- }
这样就需要再改造一下$digest,将scope传入到watch函数中:
- Scope.prototype.$digest = function() {
- var self = this;
- _.forEach(this.$$watchers, function(watcher) {
- watcher.watchFn(self);
- watcher.listenerFn(self);
- })
- }
实际运用一下:
- // 初始化一个scope
- var scope = new Scope();
- // 给scope添加属性
- scope.first = 'a';
- scope.counter = 0;
- // 准备监视first这个属性
- var watchFirst = function(scope) {return scope.first;};
- // 简化的listener,每执行一次给counter增1
- var listenerFirst = function(newVal, oldVal, scope) {scope.counter++;};
- // 给scope挂上$watch方法,目前scope.counter == 0;
- scope.$watch(watchFirst, listenerFirst);
- // 执行digest
- scope.$digest(); // scope.counter == 1
- scope.$digest(); // scope.counter == 2
- scope.$digest(); // scope.counter == 3
上面的做法是实现了循环,但每次都执行了listener函数,并没打成只对脏数据进行检查这个目的,因此我们再来修改一下digest。 增加一个last用于存储上一次的值,让它每次循环的时候存储watch的旧值然后和新值进行对比,如果对比不一致就执行listener,否则不执行。
- Scope.prototype.$watch = function(watchFn, listenerFn) {
- var watcher = {
- watchFn: watchFn,
- listenerFn: listenerFn
- };
- this.$$watchers.push(watcher);
- };
- Scope.prototype.$digest = function() {
- var self = this;
- var newValue, oldValue;
- _.forEach(this.$$watchers, function(watcher) {
- newValue = watcher.watchFn(self);
- oldValue = watcher.last;
- if (newValue != oldValue) {
- watcher.last = newValue;
- watcher.listenerFn(newValue, oldValue, self);
- }
- })
- };
初始化$watch的值
刚刚的做法虽然有个last保存上一次的值,但还没给last赋值之前,它的值将是undefined,因此我们考虑将last在初始化时作为属性赋给watcher
- // 设置一个initWatchVal()函数以供以后last初始化使用
- function initWatchVal() {}
- Scope.prototype.$watch = function(watchFn, listenerFn) {
- var watcher = {
- watchFn: watchFn,
- listenerFn: listenerFn,
- last: initWatchVal
- };
- this.$$watchers.push(watcher)
- }
- Scope.prototype.$digest = function() {
- var self = this;
- var newValue, oldValue;
- _.forEach(this.$$watchers, function(watcher) {
- newValue = watcher.watchFn(self);
- oldValue = watcher.last;
- if (newValue != oldValue) {
- watcher.last = newValue;
- // listenerFn的oldvalue也做一点调整,检查oldValue是否和initValue一样,一样的话还是让它等于newValue,因为这表示它是第一次调用,没有数据还未被污染
- watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self);
- }
- })
- };
有脏数据则保持循环
上面已经完成了部分核心功能,但还不够完善。当有多个监视数据时,有可能出现这样的情况:
- var scope = new Scope();
- scope.name = 'Jane';
- // 监视数据scope.nameUpper
- scope.$watch(
- function(scope) {return scope.nameUpper},
- function(newValue, oldValue, scope) {
- if (newValue) {
- scope.first = newValue.substring(0, 1);
- }
- }
- );
- // 监视数据scope.name
- scope.$watch(
- function(scope) {return scope.name},
- function(newValue, oldValue, scope) {
- if (newValue) {
- scope.nameUpper = newValue.toUpperCase();
- }
- }
- )
- scope.$digest(); // scope.first 期望值是'J', 实际是空
- scope.name = 'Bob';
- scope.$digest(); // scope.first 期望值是'B', 实际是'B'
以上的代码,监视了两个scope的两个数据,但由于推入的顺序问题,导致第一次执行循环时,先执行到scope.nameUpper是undefined因此无法获得scope.first的值。这样明显是有问题的。我们希望的是,无论先后加入监视,都能在最后的listener中正确执行。因此我们这样改造一下代码,对数据进行循环检查,保证不会因为undefined而出现这样的情况:
- Scope.$$digestOnce = function() {
- var self = this;
- var newValue, oldValue, dirty;
- _.forEach(this.$$watchers, function(watcher) {
- newValue = watcher.watchFn(self);
- oldValue = watcher.last;
- if (newValue != oldValue) {
- watcher.last = newValue;
- watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self);
- dirty = true;
- }
- });
- return dirty;
- };
- Scope.$digest = function() {
- var dirty;
- do {
- dirty = this.$$digestOnce();
- } while (dirty);
- };
这样在listener的新老值不一样时,会使得dirty为真,导致再执行一次$$digestOnce。一直要到循环到新老值完全一样时才跳出循环。 这样再来执行刚刚的两次$digest()代码,会发现,第一个$digest在执行时,第一次进入$$digestOnce时执行了两个监视数据的listenerFn(scope.first为空,scope.nameUpper=='JANE'),此时dirty为true,导致这里执行第二个$$digestOnce,而这次由于上次循环时已经获得了scope.nameUpper的值,所以此时便可以获得scope.first的值为'J'了。直到新老值完全一样后,dirty不再为true,便可以退出循环。
终止不稳定的循环
按上面的做法,虽然是能通过多次循环来达到同步数据的目的,但是如果多个数据之间出现相互调用,那么可能让循环进入恶性循环,无法终止。因此这里再给$digest增加一条次数上的限制:
- Scope.prototype.$digest = function() {
- var ttl = 10;
- var dirty;
- do {
- dirty = this.$$digestOnce();
- if (dirty && !(ttl--)) {
- throw "超出10次循环限制了!"
- }
- } while (dirty)
- }
NaN
在JS里,NaN是不等于它自身的。因此在angular的scope里,如果一个scope的属性是NaN,那么它是不会被加入watch的循环的。因为如果监视了它,会发现做脏检查时它始终都为脏,newValue和oldValue的值始终都不会一样。 这里可以在scope的原型上增加一个判断值的方法
- Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
- if (valueEq) {
- return _.isEqual(newValue, oldValue);
- } else {
- return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue));
- }
- }
$apply —— 整合digest循环
scope里最有名的方法之一大概就是$apply了,它被当做是调用digest的外部标准库。
- // $eval用于在scope的上下文中执行一段代码expr
- Scope.prototype.$eval = function(expr, locals) {
- return expr(this, locals)
- };
- Scope.prototype.$apply = function(expr) {
- try {
- return this.$eval(expr);
- } finally {
- this.$digest();
- }
- }
- // 实际使用中,保证代码的执行和加入digest监视循环
- scope.$apply(function(scope) {
- scope.val = 'something';
- });
到这里基本的原理就是这样的了,当然实际上的代码会比这个复杂很多,功能也要完善得多。这里只讲了最核心的一点功能的思路,如果对这个有兴趣的朋友还是直接看源码更佳。
评论