软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> Web前端 -> 构建自己的AngularJS -> 正文阅读
Web前端 最新文章
10分钟
SSM框架SSM项目源码SSM源码下载java框架整合
javascript入门
JavaScript常用对象Array(2)
8.Smarty3:模版中的内置函数
表单脚本
iTextSharp5.0页眉页脚及Asp.net预览的实现
MVC基础学习—理论篇
JavaScript
http协议中get与post区别详解

[Web前端]构建自己的AngularJS

  2016-04-01 16:49:20

作用域

第一章 作用域和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  
360图书馆 论文大全 母婴/育儿 软件开发资料 网页快照 文字转语音 购物精选 软件 美食菜谱 新闻中心 电影下载 小游戏 Chinese Culture
生肖星座解梦 人民的名义 人民的名义在线看 三沣玩客 拍拍 视频 开发 Android开发 站长 古典小说 网文精选 搜图网 天下美图
中国文化英文 多播视频 装修知识库
2017-4-30 15:06:46
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --