创建自己的AngularJS - 作用域继承(一)

作用域

作用域继承(一)

Angular作用域继承机制直接建立在Javascript原型继承基础上,并在其根部加入了一些内容。这意味着当你理解了Javascript原型链后,将对Angular作用域继承有深入了解。

根作用域

到目前为止,我们一直在和一个作用域对象打交道,该作用域使用Scope构造函数创建:

var scope = new Scope();

根作用域就是这样创建的。之所以称之为根作用域,是因为他没有父作用域,它是典型的一个由所有作用域组成的树的根。

在现实中,你永远不用用这种方式来创建一个作用域。在Angular应用中,只用一个根作用域(可以通过注入$rootScope取到它)。其他所有的作用域都是其后代,为控制器和指令创建。

创建一个子作用域

可以通过在当前作用域上调用一个名为$new的函数来创建一个子作用域。先来添加几个测试案例:

test/scope_spec.js

it("inherits the parent's properties", function() {
    var parent = new Scope();
    parent.aValue = [1, 2, 3];

    var child = parent.$new();

    expect(child.aValue).toEqual([1, 2, 3]);
});

这件事反过来不正确。一个定义在子作用域上的属性不存在于其父作用域上:

test/scope_spec.js

it("does not cause a parent to inherit its properties", function() {
    var parent = new Scope();

    var child = parent.$new();
    child.aValue = [1, 2, 3];

    expect(parent.aValue).toBeUndefined();
});

和父作用域共享属性和该属性定义的时间无关。当有一个父作用域的属性被定义,所有存在的子作用域都能获取到该属性:

test/scope_spec.js

it("inherits the parent's properties whenever they are defined", function(){
    var parent = new Scope();
    var child = parent.$new();

    parent.aValue = [1, 2, 3];

    expect(child.aValue).toEqual([1, 2, 3]);
});

你也可以通过子作用域操作父作用域中的属性,因为他们只想同一个值:

test/scope_spec.js

it("can manipulate a parent scope's property", function(){
    var parent = new Scope();
    var child = parent.$new();
    parent.aValue = [1, 2, 3];

    child.aValue.push(4);
    expect(child.aValue).toEqual([1, 2, 3, 4]);
    expect(parent.aValue).toEqual([1, 2, 3, 4]);
});

你同样可以在子作用域中监控父作用域的属性:

test/scope_spec.js

it("can watch a property in the parent", function(){
    var parent = new Scope();
    var child = parent.$new();
    parent.aValue = [1, 2, 3];
    child.counter = 0;

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

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

    parent.aValue.push(4);
    child.$digest();
    expect(child.counter).toBe(2);
});

你可以已经注意到了,子作用域也有我们定义在Scope.prototype上的$watch函数,这和我们自定义的属性是一样的继承机制:因为父作用域继承了Scope.prototype,同时子作用域又继承了父作用域,所以所有作用域都能够获取到Scope.prototype

最终,上面所讨论的对于任意深度的作用域层次都适用:

test/scope_spec.js

it("can be nested at any depth", function(){
    var a = new Scope();
    var aa = a.$new();
    var aaa = aa.$new();
    var aab = aa.$new();
    var ab = a.$new();
    var abb = ab.$new();

    a.value = 1;

    expect(aa.value).toBe(1);
    expect(aaa.value).toBe(1);
    expect(aab.value).toBe(1);
    expect(ab.value).toBe(1);
    expect(abb.value).toBe(1);

    ab.anotherValue = 2;

    expect(abb.anotherValue).toBe(2);
    expect(aa.anotherValue).toBeUndefined();
    expect(aaa.anotherValue).toBeUndefined();
});

到目前我们指定的所有内容,实现起来特别直白。我们只需要用到Javascript对象继承,因为Angular作用域有意的使用Javascript的工作机制。基本上,每当你创建了一个子作用域,其父作用域充当了他的原型

让我们在Scope的原型上创建$new函数。它为当前的作用域创建子作用域,并将其返回:

src/scope.js

Scope.prototype.$new = function(){
    var ChildScope = function() {};
    ChildScope.prototype = this;
    var child = new ChildScope();
    return child;
};

在该函数中我们首先为子作用域创建了一个构造函数,并将其当做局部变量。该构造函数不需要做任何操作,所以我们仅仅实现了一个空函数。然后我们将当前Scope赋值给ChildScope的原型。最终我们使用ChildScope构造并返回了一个新的对象。

属性遮蔽

scope继承有一个方面经常误导Angular新手,那就是遮蔽某些属性。然而这是使用Javascript原型链的直接后果。

从我们当前的测试案例可以看到我们都是在作用域上的某个属性,这样会查找原型链,如果在当前的作用域上没有找到该属性,会在其父作用域上继续查找。然后,当你在一个作用域上某个属性时,该属性只在当前作用域上存在,在父作用域上不存在。

test/scope_spec.js

it("shadows a parent's property with the same name", function(){
    var parent = new Scope();
    var child = parent.$new();

    parent.name = 'Joe';
    child.name = 'Jill';

    expect(parent.name).toBe("Joe");
    expect(child.name).toBe("Jill");
});

当我们给子作用域分配了一个已经在父作用域上存在的属性,他并不会改变父作用域。实际上,在作用域链上我们有两个不同的属性,它俩都叫name。这通常被描述为遮盖:从子作用域的角度,父作用域的name属性被子作用域上的name属性遮盖了。

这就是困惑的根源,当然也有真正的修改父作用域的使用案例。为了绕过这条规则,一个经常使用的模式是用一个对象来包装属性。该对象里的内容是可以被改变的(和上一章节中的数组操作的例子类似):

test/scope_spec.js

it("dose not shadow members of parent scope's attributes", function(){

    var parent = new Scope();
    var child  = parent.$new();

    parent.user = {name: 'Joe'};
    child.user.name = 'Jill';

    expect(child.user.name).toBe('Jill');
    expect(parent.user.name).toBe('Jill');
});

这能工作的原因是,我们没有给子作用域附加任何属性。我们只是了该作用域上user的属性,并在这个对象上添加了一些内容。这两个作用域都有一个引用指向了同一个user对象,该对象是一个简单JavaScript对象,并且和作用域继承没有任何关系。

这种模式也成为点原则,指的是对于能够使作用域发生改变的表达式,你应该在获取属性的时候使用.操作符。正如Miöko Hevery所说,“不论你什么时候使用ngModel,在里面的某处一定有一个点。如果没有,那么你做错了。”

分离监控

我们已经看到我们能够在子作用域上添加监控,因为子作用域继承了父作用域的所有方法,包括$watch$digest。但是监控到底被存储在哪里?被执行的是哪个作用域上的监控?

在我们当前的实现中,实际上所有的监控都被存储在根作用域上。这是因为我们在Scope上定义了$$watchers数组,在根作用域的构造函数上。任何一个子作用域都能获取到$$watchers数组(或者在构造器中定义的其他属性),通过属性链,他们获得了根作用域的一个拷贝。

这有一个重要含义:不论我们在哪个作用域上调用$digest,我们会在作用域层次结构上执行所有的监控。因为这里只有一个监控数组:在根作用域上的监控数组。这不是我们所需要的。

我们所希望的是当我们调用$digest函数,只是循环遍历我们调用的作用域以及其子作用域上的监控。而不是现在发生的调用其父作用域和子作用域上的监控:

test/scope_spec.js

it("does not digest its parent(s)", function(){
    var parent = new Scope();
    var child = parent.$new();

    parent.aValue = 'abc';
    parent.$watch(
        function(scope) { return scope.aValue;},
        function(newValue, oldValue, scope){
            scope.aValueWas = newValue;
        }
    );

    child.$digest();
    expect(child.aValueWas).toBeUndefined();
});

这个测试失败了,因为当我们调用child.$digest(),我们实际上在执行父作用域上的监控。让我们来修改这个问题:

解决方案是给每个子作用域非配自己的$$watchers数组:

src/scope.js

Scope.prototype.$new = function(){
    var ChildScope = function() {};
    ChildScope.prototype = this;
    var child = new ChildScope();
    child.$$watchers = [];
    return child;
};

你可能注意到我们在此使用了我们上一章节讨论的属性遮盖。每个子作用域的$$watchers数组遮盖了其父作用域上的属性。层次结构中的每个作用域有其自己的监控。当我们调用了某个作用域上的$digest,只有该作用域上的监控被执行了。

digest递归

在前一章节,我们讨论了调用$digest时不应该执行沿着层次结构向上执行监控。而是应该沿着层次结构向下执行,即执行我们调用的作用域的所有子作用域。这是有意义的,因为有些子作用域可以通过属性链监控上层作用域的属性。

既然现在我们的每一个作用域都有一个独立的监控数组,这代表着当我们调用父作用域上的$digest时,子作用域上的$digest不会被执行。我们现在需要修改$digest:当改变$digest时,不仅当前作用域会工作,其子作用域也会。

我们遇到的第一个问题是:当前的作用域不知道他是否含有子作用域,也不知道他的子作用域是谁。我们需要绕过每个作用域能够追踪到其子作用域。根作用域和子作用域都需要。让我们将这些作用域存储在一个数组中,命名为$$children

test/scope_spec.js

it("keeps a record of its children", function(){
    var parent = new Scope();
    var child1 = parent.$new();
    var child2 = parent.$new();
    var child2_1 = child2.$new();

    expect(parent.$$children.length).toBe(2);
	expect(parent.$$children[0]).toBe(child1);
    expect(parent.$$children[1]).toBe(child2);

    expect(child1.$$children.length).toBe(0);

    expect(child2.$$children.length).toBe(1);
	expect(child2.$$children[0]).toBe(child2_1);
});

我们需要在根作用域的构造函数中初试化$$children

src/scope.js

function Scope(){
    this.$$watchers = [];
	this.$$lastDirtyWatch = null;
    this.$$asyncQueue = [];
	this.$$applyAsyncQueue = [];
    this.$$applyAsyncId = null;
	this.$$postDigestQueue = [];
    this.$$children = [];
	this.$$phase = null;
}

然后我们需要在创建子作用域时,将其加入到该数组中。我们还需给子作用域分配自己的$$children数组(遮盖了其父作用域的同名属性),当我们遍历监控时不会遇到相同的问题。这些变化都在$new函数中:

src/scope.js

Scope.prototype.$new = function(){
    var ChildScope = function() {};
    ChildScope.prototype = this;
    var child = new ChildScope();
    this.$$children.push(child);
	child.$$watchers = [];
    child.$$children = [];
    return child;
};

现在我们能够记录子作用域,我们将要讨论他们的digest循环。在父作用域上调用$digest也能够执行子作用域中的监控:

test/scope_spec.js

it("digests its children ", function(){
    var parent = new Scope();
    var child = parent.$new();

    parent.aValue = 'abc';
    child.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.aValueWas = newValue;
        });

    parent.$digest();
    expect(child.aValueWas).toBe('abc');

});

注意这个测试基本上是上节测试的一个镜像,在上节测试中我们在子作用域上调用$digest不应该在父作用域上引起监控函数的执行。

为了让这个能够工作,我们需要改变$$digestOnce函数,来让监听沿着层次机构向下运行。为了更简单,首先我们先添加一个助手函数$$everyScope(根据JavaScript的Array.every来命名),该函数针对层次结构中的每一个作用域都可以执行任意一个函数,知道某个函数返回false:

src/scope.js

Scope.prototype.$$everyScope = function(fn){
	if(fn(this)){
		return this.$$children.every(function(child){
			return child.$$everyScope(fn);
        });
    }else{
        return false;
    }
};

对于当前作用域,该函数调用fn一次,然后在其子作用域上递归调用。

我们可以在$$digestOnce的外层循环中使用该函数进行操作:

src/scope.js

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

                        scope.$$lastDirtyWatch = watcher;

                        watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
                        watcher.listenerFn(newValue, 
                            (oldValue === initWatchVal ? newValue: oldValue), 
                            scope);
                        dirty = true;
                    }else if(self.$$lastDirtyWatch === watcher){//请注意这里是当前作用域的$$lastDirtyWatch进行比较,而不是scope.$$lastDirtyWatch
                        continueLoop = false;
                        return false;
                    }
                }
        } catch (e){
            console.error(e);
        }
        });
        return continueLoop;
    });
    return dirty;
};

现在$$digestOnce函数会向下向下遍历整个层次结构,并且会返回一个布尔型的值,来指示在层次结构是是否有监控室脏的。

内层循环遍历作用域的层次结构直到所有的作用域都被遍历,或者短路优化发生了作用。如果优化起作用了,将被存储在continueLoop变量中。如果该值为false,我们将会跳出循环和$$digestOnce函数。

注意到在内层循环中我们使用了一个特殊的变量scope来指向this监控函数应该传入其原始的作用域对象,而不是正在调用$digest的作用域对象

注意到$$lastDirtyWatch属性指向了最高层的作用域。短路优化应该对作用域层级上的所有监控负责。如果我们在当前作用域上设置了$$lastDirtyWatch,它将遮盖掉其父作用域的属性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值