创建自己的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,它将遮盖掉其父作用域的属性。

时间: 2024-08-07 06:54:03

创建自己的AngularJS - 作用域继承(一)的相关文章

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

作用域 第一章 作用域和Digest(五) 销毁监控 当你注册一个监控,很多时候你想让它和scope一样保持活跃的状态,所以不必显示的删除他.然而,有些情况下,你需要销毁一个特定的监控,但是仍然保持作用域可操作.意思就是,我们需要给监控增加一个删除操作. Angular实现这个的方式特别聪明:Angular中的$watch函数有一个返回值.他是一个函数,但其被调用的时候,即删除了其注册的监控.如果想要能够移除一个监控,只要存储注册监控函数时返回的函数,然后当不需要监控的时候调用它即可: test

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

作用域 第一章 作用域和Digest(四) 联合$apply调用 - $applyAsync 不论在digest里面还是外面调用$evalAsync去延迟工作,他实际是为之前的使用案例设计的.之所以在setTimeout中调用digest是为了在digest循环外面调用$evalAsync时防止混淆. 针对在digest循环外部异步调用$apply的情况,同样有一个名为$applyAsync来处理.其使用类似于$apply - 为了集成没有意识到Angular digest循环的代码.和$app

AngularJS 作用域与数据绑定机制

AngularJS 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与同为 MVC 框架的 Dojo 的定位不同,AngularJS 在功能上更加轻量,而相比于 jQuery,AngularJS 又帮您省去了许多机械的绑定工作.在一些对开发速度要求高,功能模块不需要太丰富的非企业级 WEB 应用上,AngularJS 是一个非常好的选择.AngularJS 最为复杂同

angularJS 作用域

<!doctype html><html ng-app="firstApp"><head> <meta charset="utf-8"> <script src="angular-1.3.0.js"></script></head><body><pre> </pre><div ng-controller="par

AngularJS作用域

AngularJS作用域 一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scop

剖析AngularJS作用域

一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scope: true 指令会创建

angularjs作用域之transclude

transclude是一个可选的参数.如果设置了,其值必须为true,它的默认值是false.嵌入有时被认为是一个高级主题,但某些情况下它与我们刚刚学习过的作用域之间会有非常好的配合.使用嵌入也会很好地扩充我们的工具集,特别是在创建可以在团队.项目.AngularJS社区之间共享的HTML代码片段时. 嵌入通常用来创建可复用的组件,典型的例子是模态对话框或导航栏.我们可以将整个模板,包括其中的指令通过嵌入全部传入一个指令中.这样做可以将任意内容和作用域传递给指令.transclude参数就是用来

构建自己的AngularJS - 作用域和Digest(一)

作用域 第一章 作用域和Digest(一) Angular作用域是简单javascript对象,因此你可以像对其他对象一样添加属性.然而,他们也有一些额外的功能,用于观测数据结构的变化.这种观察能力是使用脏值检查在digest循环中运行来实现的.这就是我们这一章将要实现的内容. 作用域对象 Scope的创建是通过在Scope构造函数之前加入new关键字来创建的.这样会产生一个简单javascript对象.让我们先来创建一个单元测试.(测试驱动开发,先写测试案例) 对Scope创建一个测试文件te

构建自己的AngularJS - 作用域和Digest(三)

作用域 第一章 作用域和Digest(三) $eval - 在当前作用域的上下文中执行代码 Angular有多种方式让你在当前作用域的上下文中执行代码.最简单的是$eval.传入一个函数当做其参数,然后将当前的作用域作为参数传给该函数,并执行它.然后它返回该函数的执行结果.$eval还有第二个可选的参数,它仅仅是被传递给将要执行的函数. 有几个单元测试展示了我们如何使用$eval: test/scope_spec.js it("execute $eval'ed function and retu