软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> Web前端 -> 构建自己的AngularJS -> 正文阅读

[Web前端]构建自己的AngularJS


作用域

第一章 作用域和Digest(一)


Angular作用域是简单javascript对象,因此你可以像对其他对象一样添加属性。然而,他们也有一些额外的功能,用于观测数据结构的变化。这种观察能力是使用脏值检查digest循环中运行来实现的。这就是我们这一章将要实现的内容。

作用域对象


Scope的创建是通过在Scope构造函数之前加入new关键字来创建的。这样会产生一个简单javascript对象。让我们先来创建一个单元测试。(测试驱动开发,先写测试案例)
对Scope创建一个测试文件test/scope_spec.js,并且在里面添加测试案例。
文件test/scope_spec.js
/* jshint globalstrict: true */ /* global Scope: false */
'use strict' ;
describe("Scope", function() {
it("can be constructed and used as an object", function() { 
    var scope = new Scope();
    scope.aProperty = 1;
    expect(scope.aProperty).toBe(1);
    });
});

在文件的最上面,我们开启了ES5严格模式,同时让JSHint知道可以引用一个名为Scope的全局对象。
这个测试案例仅仅是创建了一个Scope,在其上附件了一个任意属性,同时检查其是否真的被添加上。

在这里你可能会注意到我们居然使用Scope作为一个全局函数。这绝对不是一个好的JavaScript编程方式!在本书的后面,一旦我们实现了依赖注入,我们将会改正这个错误。
如果你在终端中使用了Testem,你会发现当你添加了测试案例后,测试失败了。因为我们还没有实现Scope函数。这正是我们所希望的,因为测试驱动开发的第一步是先看到错误。
在本书中我都会假设测试会自动运行,并且不会指明什么时候测试应该运行。

我们可以让这个测试案例轻松通过:创建src/scope.js,内容如下:
src/scope.js
/* jshint globalstrict: true */
'use strict' ; 
function Scope() {
}

在测试案例中,我们在scope上附加了一个属性(aProperty)。这精确的描述了Scope上的属性是如何工作的。他们是直白的JavaScript属性,和其他的属性相比没有任何特殊的地方。没有特殊的Set函数需要被调用,也不限制你所附加的属性的值。最神奇的地方是两个非常特别的函数:watchdigest。下面让我们把注意力放在他们身上。

监控对象属性:watchdigest


watchdigest 是同一个硬币的两面。他们两一起组成了digest循环的核心:对数据的变化做出反应
通过watchwatcherwatcherwatch从而来创建一个watcher:
  • 一个watcher函数,你所感兴趣的特定的内容
  • 一个listener函数,当数据发生变化的时候被调用

作为一个Angular使用者,你通常来监控表达式而不是函数。一个监控表达式是一个字符串,例如:”user.firstName”,通常你在数据绑定、指令属性或者Javascript代码中使用。Angular会将其解析并编译成watch函数。我们会在本书的第二章中实现。现在我们仅仅是直接使用watch函数。

硬币的另一面是$digest函数。他遍历作用域上的所有的watchers,并且调用他们的watch和listener函数。
为了填充Scope使其满足上面的内容,让我们先定义一个测试用例,假定你可以通过watchwatcherdigest了以后,listener函数会被触发。
为了让代码更好管理,在scope_spec.js的describe块中加入一个嵌套的块。同时创建一个beforeEache函数来初始化Scope,这样你就不用在每个测试中重复创建Scope了:
test/scope_spec.js
describe("Scope", function() {

    it("can be constructed and used as an object", function() { 
        var scope = new Scope();
        scope.aProperty = 1;
        expect(scope.aProperty).toBe(1);
      });

    describe("digest", function() {
        var scope;
        beforeEach(function() { 
            scope = new Scope();
        });
        it("calls the listener function of a watch on first $digest", function() { 
            var watchFn = function() { return  'wat' ; };
            var listenerFn = jasmine.createSpy();
            scope.$watch(watchFn, listenerFn);
                  scope.$digest();
                  expect(listenerFn).toHaveBeenCalled();
            });
        });
});

在测试案例中我们调用watchwatcherwatchlistenerJasmineSpydigest,然后来检查而listener函数确实被调用了。
为了让测试案例通过,我们需要做一些事情。首先,Scope需要有地方去存放所有注册的watcher。让我们在Scope的构造函数中创建一个数组来存放。
src/scope.js
function Scope() {
    this.$$watchers = []; 
}

两个$$前缀表示该变量是Angular框架的内部变量,在应用程序代码中不应该被调用。

现在我们可以定义watch$watchers数组中。我们希望每个Scope的对象都拥有该函数,所以我们把它加入到Scope的原型中。
src/scope.js
Scope.prototype.$watch = function(watchFn, listenerFn) { 
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn
      };
    this.$$watchers.push(watcher); 
};

最后是$digest函数。现在我们定义了一个非常简单的版本,他仅仅是遍历了所有注册的watchers函数,然后调用他们的listener函数。
src/scope.js
Scope.prototype.$digest = function() {
    _.forEach(this.$$watchers, function(watcher) {
        watcher.listenerFn();
      });
};

测试案例通过了,但是这个版本$digest还不是很有用。我们所希望的是当watch函数中的内容发生变化的时候才调用listener函数。这就是脏值检查

脏值检查


如上面所说,watch函数应该返回我们感兴趣的数据的变化。通常这些数据是Scope中的内容。为了让watch函数更方便的取到作用域中的数据,我们将当前作用域作为参数传递给watch函数。一个watch函数可能如下:
function(scope) {
    return scope.firstName; 
}

这是watch函数通常采取的形式:从作用域中取到某些值,并将其返回。
让我们增加一些测试案例,来测试在watch函数中确实传递了scope。
test/scope_spec.js
it("calls the watch function with the scope as the argument", function() { 
    var watchFn = jasmine.createSpy();
    var listenerFn = function() { };
    scope.$watch(watchFn, listenerFn);
      scope.$digest();
      expect(watchFn).toHaveBeenCalledWith(scope);
});

这次我们给watch函数创建了一个Spy,用来检测watch函数的调用。让测试通过的最简单的方法是像下面这样修改$digest函数:
src/scope.js
Scope.prototype.$digest = function() {
    var self = this;
    _.forEach(this.$$watchers, function(watcher) {
        watcher.watchFn(self);
        watcher.listenerFn();
      });
};

var self = this;在本书中我们都会用到这种方式来帮顶javascript中的this。这里有一系列文章来说明这种模式。

当然这不是我们所追求的。$digest函数的工作是调用watch函数并且和该函数上次的返回值进行比较。如果值不同,watch是脏的,他的listener函数需要被调用。让我们更进一步的为此添加一个测试案例:
test/scope_spec.js
it("calls the listener function when the watched value changes", function() { 
    scope.someValue =  'a' ;
    scope.counter = 0;
    scope.$watch(
        function(scope) { return scope.someValue; }, 
        function(newValue, oldValue, scope) { scope.counter++; }
      );
    expect(scope.counter).toBe(0);
    scope.$digest();
    expect(scope.counter).toBe(1);
    scope.$digest();
    expect(scope.counter).toBe(1);
    scope.someValue =  'b' ;
    expect(scope.counter).toBe(1);
    scope.$digest();
    expect(scope.counter).toBe(2);
});

首先我们在scope上定义了两个属性:一个字符串和一个数字。然后我们添加了一个watcher来监控字符串,当其变化是数字自增。期望是当第一次调用digestcounterdigest后,counter自增。
你应该注意到listener函数的参数发生了变化,和watcher函数一样,他需要scope作为参数,同时他也需要watcher的新值和旧值作为参数。这让开发者能够更容易的检查属性到底发生了什么变化。
为了让上述的代码能够工作,digestwatchwatcher便digest函数的新的定义,对于每个watcher函数他检查其值是否改变。
src/scope.js
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);
            }
    }); 
};

对于每个watcher函数,我们将函数的返回值和之前我们在last属性中已经存储的值。如果两个值不一致,我们调用listener函数,并将新值和旧值还有scope一起传递给他。最后,我们更新last的值。
到现在为止,我们已经实现了Angular作用域的精华:添加watcher函数并且在digest中调用他们
我们同样看到了Angular作用域中的一些重要特性:
- 在作用域中添加属性并不会对性能产生影响。如果一个属性没有watcher函数监控它,他不在作用域上也没有关系。Angular并不会遍历scope上的每个属性。他只会遍历所有的watch函数。
- 在每一次$digest循环中,每个watch函数都会被调用。因为这个原因,你需要关注watch函数的数量和每个watch函数或者表达式的性能。

初始化监视值


将一个监视函数的返回值同存储在last中的属性进行比较在大多数情况下是有效的,但是当监视器函数第一次执行时会是什么情形呢?既然此时我们还没有设置last属性,它的值的undefined。而如果此时监听器的合法值是undefined,watch函数将不会运行:
test/scope_spec.js
it("calls listener when watch value is first undefined", function() { 
  scope.counter = 0;
  scope.$watch(
        function(scope) { return scope.someValue; },
        function(newValue, oldValue, scope) { scope.counter++; }
  );
  scope.$digest();
  expect(scope.counter).toBe(1);
});

在上面的测试案例中listener函数应该被调用。我们需要做的是初试化last属性,并且保证其应该是独一无二的。这样可以和watch函数的返回值进行区分。
函数满足了上述需求,因为javascript函数是所谓的引用值 - 除了自己他们和其他任何值都不想等。让我们在scope.js的最上面引入该函数。
src/scope.js
function initWatchVal() { }

现在我们将该函数作为last的属性添加到监控函数中:
src/scope.js
Scope.prototype.$watch = function(watchFn, listenerFn) { 
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn,
        last: initWatchVal
    };
    this.$$watchers.push(watcher); };

通过这种方法,watch函数会确保不论函数的返回值是什么,listener函数被调用。
尽管initWatchVal解决了watch函数的旧值,我们最好不要将该函数写在scope.js的外面。对于第一次调用watch,我们应该同时将新值作为旧值传递给listener。
test/scope_spec.js
it("calls listener with new value as old value the first time", function() { 
    scope.someValue = 123;
    var oldValueGiven;
    scope.$watch(
        function(scope) { return scope.someValue; },
        function(newValue, oldValue, scope) { oldValueGiven = oldValue; }
        );
    scope.$digest();
    expect(oldValueGiven).toBe(123);
});

在$digest中,我们检查旧值是否是初始化的值,如果是,我们将用新值替换它:
src/scope.js
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 === initWatchVal ? newValue : oldValue),
        self); 
        }
    }); 
};

得到Digest的通知


如果你想要每次Angular的digest循环都得到通知,你可以利用这个事实:在digest中每个watch都会被调用:仅仅需要注册一个没有listener的watch函数。为此我们添加一个测试案例
test/scope_spec.js
it("may have watchers that omit the listener function", function() { 
    var watchFn = jasmine.createSpy().and.returnValue( 'something' ); scope.$watch(watchFn);
    scope.$digest();
    expect(watchFn).toHaveBeenCalled();
});

像这样的案例watch不需要返回任何结果,但是它可以返回,就想这个案例一样。当scope正在digest循环中抛出了一个异常。这是因为我们正在尝试调用一个不存在的listener函数。为了支持使用该案例,我们需要检查listener函数在$watch是否被省略,如果省略,在该位置上放置一个空的实现的函数。
src/scope.js
Scope.prototype.$watch = function(watchFn, listenerFn) { 
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function() { }, 
        last: initWatchVal
    };
    this.$$watchers.push(watcher); 
};

如果你使用了这种模式,请记住Angular会查看watchFn的返回值,即使listeFn不存在。如果你返回了一个值,该值仍会进行脏值检查。为了保证你使用该模式不产生额外的工作,请不好返回任何值。在这种情况下,watch的值恒为undefined。

当有脏值的情况下保持Digest循环


最核心的实现在那里,但是我们仍然远远没有完成。例如,有一个很经典的场景我们还没有支持:listener函数本身可能改变作用域上的属性。如果发生了,同时有一个watcher函数正在监听刚改变的属性,这可能在同一个digest循环中不会被注意到:
test/scope_spec.js
it("triggers chained watchers in the same digest", function(){
    scope.name = 'Jane';

    scope.$watch(
        function (scope){ return scope.nameUpper; },
        function(newValue, oldValue, scope){
            if(newValue){
                scope.initial = newValue.substring(0, 1) + ".";
            }
        });

    scope.$watch(
        function(scope) { return scope.name; },
        function(newValue, oldValue, scope){
            if(newValue){
                scope.nameUpper = newValue.toUpperCase();
            }
        });

    scope.$digest();

    expect(scope.initial).toBe('J.');
    scope.name = 'Bob';
    scope.$digest();
    expect(scope.initial).toBe('B.');
});

在这个作用域上我们有两个watch函数:一个监控nameUpper属性,基于它分配了initial属性,另一个监控name属性,基于它分配了nameUpper属性。我们希望发生的是当scope上的name属性发生变化时,nameUpperinitial在digest循环后都更新。然而,测试案例中并不是这样。

我们故意安排监控函数的顺序,让依赖的那个先注册。如果顺序反转了,测试会立马通过,因为监控函数以正确的顺序发生。然后,像我们即将看到的那样,不同监控函数中的依赖应该和他们的注册顺序无关

我们需要去修改digest函数让其能够一直遍历监控函数,直到监控值停止变化。做多次循环是我们能够是依赖其他的监控函数被应用的唯一方法。
首先,我们将当前的digest$digestOnce,调整它使其能够一次运行所有的监控函数,并且放回一个布尔值来决定是否有其他变化。
src/scope.js
Scope.prototype.$$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;
};

然后,让我们重新定义digest$digestOnce:
src/scope.js
Scope.prototype.$digest = function(){
    var dirty;
    do {
        dirty = this.$$digestOnce();
    } while (dirty);
};

现在$digest会调用所有的监控函数至少一次。如果在第一次中,任一个监控的值有变化,该次被标记为脏值,所有的监控函数会运行第二次。这将继续运行直到没有监控的值发生变化,这样被认为稳定情况。

Angular作用域实际上没有一个名为$$digestOnce的函数。作为替代,digest循环全都嵌套在$digest中。我们的目的是清晰的性能,所以为了我们的目的,提取出内部循环作为函数是有意义的。

对于Angular监控函数我们可以有更进一步的观察:在每一次digest循环中他们可能运行多次。这就是人们经常说监控应该满足幂等性:一个监控函数应该没有任何副作用,或者副作用能够发生有限的次数。例如,一个监控函数触发了一个Ajax请求,不能保证你的应用到底能制造多少次请求(即请求的次数不确定??)。
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-04-01 16:48:17  
Web前端 最新文章
10分钟
SSM框架SSM项目源码SSM源码下载java框架整合
javascript入门
JavaScript常用对象Array(2)
8.Smarty3:模版中的内置函数
表单脚本
iTextSharp5.0页眉页脚及Asp.net预览的实现
MVC基础学习—理论篇
JavaScript
http协议中get与post区别详解
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture
生肖星座 三丰软件 视频 开发 Android开发 站长 古典小说 网文精选 搜图网 美图 中国文化英文版 多播 租车 短信
2017-7-26 10:44:00
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --