码迷,mamicode.com
首页 > Web开发 > 详细

创建自己的AngularJS - 作用域和Digest(五)

时间:2016-04-18 11:59:02      阅读:451      评论:0      收藏:0      [点我收藏+]

标签:

作用域

第一章 作用域和Digest(五)

销毁监控

当你注册一个监控,很多时候你想让它和scope一样保持活跃的状态,所以不必显示的删除他。然而,有些情况下,你需要销毁一个特定的监控,但是仍然保持作用域可操作。意思就是,我们需要给监控增加一个删除操作。

Angular实现这个的方式特别聪明:Angular中的$watch函数有一个返回值。他是一个函数,但其被调用的时候,即删除了其注册的监控。如果想要能够移除一个监控,只要存储注册监控函数时返回的函数,然后当不需要监控的时候调用它即可:

test/scope_spec.js

it("allows destroying a $watch with a removal function", function(){
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    var destroyWatch = scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter ++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(1);

    scope.aValue = ‘def‘;
    scope.$digest();
    expect(scope.counter).toBe(2);

    scope.aValue = ‘ghi‘;
    destroyWatch();
    scope.$digest();
    expect(scope.counter).toBe(2);

});

为了实现该功能,我们需要在$watch中返回一个函数,并将其在$$watchers数组中删除:

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.push(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
        }
    }
};

在监听函数能够删除他本身的之前,我们还有一些测试案例需要去处理,来保证我们有一个健壮的实现。他们在digest循环中删除监控。

首先,一个监控可能在他自己的监控函数或者监听函数中删除他自己。这不应该影响到其他监控:

test/scope_spec.js

it("allows destroying a $watch during digest", function(){
    scope.aValue = ‘abc‘;

    var watchCalls = [];

    scope.$watch(
        function(scope) {
            watchCalls.push("first");
            return scope.aValue;
        }
    );

    var destroyWatch = scope.$watch(
        function(scope) {
            watchCalls.push(‘second‘);
            destroyWatch();
        }
    );

    scope.$watch(
        function(scope) {
            watchCalls.push(‘third‘);
            return scope.aValue;
        }
    );

    scope.$digest();
    expect(watchCalls).toEqual([‘first‘, ‘second‘, ‘third‘, ‘first‘, ‘third‘]);
});

在这个测试中我们有三个监控。中间的监控当它第一次被调用的时候他删除了他自己,只留下了第一个和第三个。我们验证监控以正确的顺序被遍历:在第一次遍历时,每个监控被执行一次。然后,因为digest是脏的,每个监控再次被执行,但是现在第二个监控已经不存在了。

然而,事实上,当第二个监控移除掉他自己时,监控集合中第二个右边的内容左移一位,导致$$digestOnce在本次循环中跳过了第三个监控。

译者注:对于_.forEach函数,在第二个被移除后,第三个监控的数组下标变为了1,然后进入循环进入第三次,即访问$$watchers[2],该内容已不存在,为undefined,从而导致了原来数组的第三个监控,即现在的第二个,没有被遍历。

解决这个问题的窍门就是要反转$$watches数组,所有新的监控被加到数组的前面,然后从后面向前遍历数组。当一个监控被移除掉,被移动的部分已经在digest遍历中被处理了,它不会影响剩余的部分。

当我们添加一个监控时,我们要使用Array.unshift来代替Array.push

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.unshift(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
        }
    };
};

然后,当遍历监控时,我们应该使用_.forEachRight来代替_.forEach,用来反转数组顺序:

src/scope.js

Scope.prototype.$$digestOnce = function(){
	var self = this;
	var newValue, oldValue, dirty;
	_.forEachRight(this.$$watchers, function(watcher){
		try{
			newValue = watcher.watchFn(self);
			oldValue = watcher.last;
			if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){

                self.$$lastDirtyWatch = watcher;

                watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
                watcher.listenerFn(newValue, 
                    (oldValue === initWatchVal ? newValue: oldValue), 
                    self);
                dirty = true;
            }else if(self.$$lastDirtyWatch === watcher){
                return false;
            }
    } catch (e){
        console.error(e);
    }
    });
    return dirty;
};

下一个案例是移除另一个监控。观察下面的测试案例:

test/scope_spec.js

it("allows a $watch to destroy another during digest", function() {
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    scope.$watch(
        function(scope) {
            return scope.aValue;
        },
        function(newValue, oldValue, scope) {
            destroyWatch();
        }
    );

    var destroyWatch = scope.$watch(
        function(scope) {},
        function(newValue, oldValue, scope) {}
    );

    scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter ++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(1);
});

测试失败了。罪魁祸首是我们的短路优化。回忆一下,在$$digestOnce中,如果当前的监控是上一次遍历中那个脏的,并且现在干净了,我们会结束掉digest。在这个测试案例中发生的步骤如下:

  1. 第一个监控被执行了。它现在是脏的,它被存储在$$lastDirtyWatch中,并且它的监听被执行了。该监听销毁了第二个监控。
  2. 第一个监控再次被执行,因为在监控数组中它被向前移动了一个位置。这次他是干净的,并且因为他还和$$lastDirtyWatch相等,digest结束了。我们永远没有进入第三个监控。

在有监控被移除的情况下,我们应该取消短路优化,让这种情况不会发生。

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.unshift(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
			self.$$lastDirtyWatch = null;
        }
    };
};

最后一个测试考虑当在一个监控中同时移除多个监控的情况:

test/scope_spec.js

it("allows destroying several $watches during digest", function(){
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    var destroyWatch1 = scope.$watch(
        function(scope){
            destroyWatch1();
            destroyWatch2();
        });

    var destroyWatch2 = scope.$watch(
        function(scope){ return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(0);
});

第一个监控不仅销毁了他本身,而且销毁了即将执行的第二个监控。既然我们不希望第二个监控执行,我们也不希望他抛出异常,也就是他现在正在发生的情况。

我们能做的就是在迭代时检查当前的监控是否存在:

src/scope.js

Scope.prototype.$$digestOnce = function(){
	var self = this;
	var newValue, oldValue, dirty;
	_.forEachRight(this.$$watchers, function(watcher){
		try{
			if(watcher){
				newValue = watcher.watchFn(self);
				oldValue = watcher.last;
				if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){

                    self.$$lastDirtyWatch = watcher;

                    watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
                    watcher.listenerFn(newValue, 
                        (oldValue === initWatchVal ? newValue: oldValue), 
                        self);
                    dirty = true;
                }else if(self.$$lastDirtyWatch === watcher){
                    return false;
                }
            }
    } catch (e){
        console.error(e);
    }
    });
    return dirty;
};

最终我们可以放心digest将会忽略被移除掉的监控而继续运行。

在一个监听中监控多个变化:$watchGroup

到目前为止我们的监控和监听只是简单的因果对:当监控发生变化,启动监听。这很不常用,然而,更常用的是监控多个状态,当他们中任一个改变时,执行同样的代码。

因为Angular监控只是普通的Javascript函数,在我们当前已经实现的监控函数下这完全可以实施:创建一个监控函数,运行多个检查,返回他们的集合的值,并且触发监听函数。

但是从Angular1.3起你不用自己手动创建这样的函数。而是可以使用Scope中已经构建好的一个特性:$watchGroup

$watchGroup函数将多个监控函数包装成一个对象,和一个监听函数。思想是:当任何数组中的一个监控函数检测到了变化,监听函数就会被调用。监听函数会接收新值和旧值包装成的数组作为参数,其顺序和原始的监听函数一样。

下面有一个测试案例,被封装在了新的describe块中:

test/scope_spec.js

describe(‘$watchGroup‘, function(){
    var scope;
    beforeEach(function(){
        scope = new Scope();
    });

    it(‘takes watches as an array and calls listener with arrays‘, function(){
        var gotNewvalues, gotOldValues;

        scope.aValue = 1;
        scope.anotherValue = 2;

        scope.$watchGroup([
            function(scope) { return scope.aValue;},
            function(scope) { return scope.anotherValue; }
        ], function(newValue, oldValue, scope){
            gotNewvalues = newValue;
            gotOldValues = oldValue;
        });
        scope.$digest();

        expect(gotNewvalues).toEqual([1, 2]);
        expect(gotOldValues).toEqual([1, 2]);

    });
});

在测试中,我们抓取了监听函数中接收的newValuesoldValues参数,检查他们的值是否是监听函数返回值组成的数组。

让我们首先尝试实现$watchGroup。我们尝试每个监控单独注册,并对其使用同一个监听函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    _.forEach(watchFns, function(watchFn){
        self.$watch(watchFn, listenerFn);
    });
};

但是这不是很贴切。我们希望监听函数接受所有监控值组成的数组,但是现在仅仅是分别得到了分别调用的监控值。

对于每个监控,我们还需要为其定义一个单独的内部的监听函数,并且在这些内部监听的里面收集了所有的监控值组成的数组。然后我们将这些数组传递给原始的监听函数。我们使用一个数组来存储新值,一个存储旧值:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            listenerFn(newValues, oldValues, self);
        })
    });
};

$watchGroup永远使用引用来检测值的变化。

我们现在的实现有一个问题,他调用监听有点太早了:如果监控数组中有几次变化,监听函数就会被调用几次,我们更希望之调用他一次。更差的是,因为一旦发现了变化,我们会立即调用监听,很有可能在新值和旧值的数组中会将以前的值和现在值的混淆,从而让用户看到不一致的值的组合。

即使有多个变化,监听函数页应该之调用一次,让我们测试一下:

test/scope_spec.js

it("only calls listener once per digest", function(){
    var counter = 0;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue;}, 
        function(scope) { return scope.anotherValue;}
    ], function(newValues, oldValues, scope){
        counter++;
    });

    scope.$digest();
    expect(counter).toBe(1);
});

我们怎样推迟监听函数的调用知道所有的监听都被检查了呢?既然在$watchGroup中,我们不负责调用digest,没有明显的地方让我们去掉用监听函数。但是,我们可以使用在之前实现的$evalAsync函数。它的目的就是推迟某些工作的执行,但是仍在同一个digest中 - 这正是我们所需要的!

我们在$watchGroup中创建一个内部函数,名叫watchGroupListener。这个函数负责用两个数组来调用原来的监听函数。然后,在这个函数没有被调度的情况下,在每个单独的监听中我们调度这个函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;

    function watchGroupListener(){
        listenerFn(newValues, oldValues, self);
        changeReactionScheduled = false;
    }

    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        })
    });
};

这处理了基本的$watchGroup的行为,现在我们将注意力放在几个特殊的情况上。

有一个问题是和第一次调用监控函数有关的,新值和旧值应该是一样的。现在,我们的$watchGroup就是这样做的,因为他是基于$watch函数实现的这个行为。在第一次调用的情况下,新值和旧值数组一定一样。

然而,尽管这两个数组是一样的,但是他们仍然是两个独立的数组对象。这打破了使用同一个严格相等的值两次的约定。这同时意味着如果一个用户想要去比较这两个值,他们不能使用引用相等(===),而是需要迭代数组的内容,查看是否匹配。

我们想要做的更好,在第一次调用的时候,让新值和旧值严格相等:

test/scope_spec.js

it(‘uses the same array of old and new values on first run‘, function(){
    var gotNewvalues, gotOldValues;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue}
    ], function(newValues, oldValues, scope) {
        gotNewvalues = newValues;
        gotOldValues = oldValues;
    });

    scope.$digest();
    expect(gotNewvalues).toBe(gotOldValues);
});

在这样做的情况下,让我们同时确认我们不会打破我们现有的,通过添加一个测试来确保我们在监听函数中仍然得到的是不同的数组:

test/scope_spec.js

it("uses different arrasy for old and new values on subsequent runs", function(){
    var gotNewValues, gotOldValues;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue; }
    ], function(newValues, oldValues, scope){
        gotNewvalues = newValues;
        gotOldValues = oldValues;
    });

    scope.$digest();

    scope.anotherValue = 3;
    scope.$digest();

    expect(gotNewvalues).toEqual([1, 3]);
    expect(gotOldValues).toEqual([1, 2]);
});

我们可以通过检查监听函数是否是第一次被调用来实现这个需求。如果是第一次被调用,我们仅仅是将newValues数组传递给监听函数两次:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });
};

我们需要$watchGroups的最后一个特性是:注销登记。我们应该可以向注销单个监控一样注销监控数组。通过使用$watchGroups返回的注销函数。

test/scope_spec.js

it(‘can be deregistered‘, function() {
    var counter = 0;

    scope.aValue = 1;
    scope.anotherValue = 2;

    var destroyGroup = scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue; }
    ], function(newValues, oldValues, scope){
        counter ++;
    });
    scope.$digest();

    scope.anotherValue = 3;
    destroyWatch();
    scope.$digest();
    expect(counter).toBe(1);

});

这里我们测试了一旦调用了注销函数,即使监控值发生变化,也不会引起监听函数被触发。

既然单个的监听已经返回了注销函数,我们要做的是收集他们,然后创建一个注销函数来调用所有单个注销函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    var destroyFunctions = _.map(watchFns, function(watchFn, i){
        return self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });

    return function(){
        _.forEach(destroyFunctions, function(destroyFunction) {
            destroyFunction();
        });
    };
};

我们有一个监控数组为空的特殊的测试案例,同样需要监听注销函数能够正常工作。在这种情况下,监听函数只被调用一次,但是仍然可以在第一次digest循环发生之前调用注销函数,在这种情况下单次调用应该被跳过:

test/scope_spec.js

it("does not call the zero-watch listen when deregistered first", function(){
    var counter = 0;

    var destroyGroup = scope.$watchGroup([], function(newValues, oldValues, scope){
        counter ++;
    });
    destroyGroup();
    scope.$digest();

    expect(counter).toEqual(0);

});

对于这种情况注销函数应该设置一个布尔型的标识,在调用监听函数之前先检查其值:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    if(watchFns.length === 0){
        var shouldCall = true;
        self.$evalAsync(function() {
            if(shouldCall){
                listenerFn(newValues, oldValues, self);
            }
        });
        return function(){
            shouldCall = false;
        };
    }

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    var destroyFunctions = _.map(watchFns, function(watchFn, i){
        return self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });

    return function(){
        _.forEach(destroyFunctions, function(destroyFunction) {
            destroyFunction();
        });
    };
};

总结

到现在为止,我们终于拥有了一个完美的Angular风格的脏值检查作用域系统。

技术分享

创建自己的AngularJS - 作用域和Digest(五)

标签:

原文地址:http://blog.csdn.net/fangjuanyuyue/article/details/51177520

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!