作用域

第一章 作用域和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函数须要被调用,也不限制你所附加的属性的值。

最奇妙的地方是两个非常特别的函数:$watch$digest

以下让我们把注意力放在他们身上。

监控对象属性:watch和digest

$watch$digest 是同一个硬币的两面。

他们两一起组成了digest循环的核心:对数据的变化做出反应

通过$watch你能够在作用域上附加一个watcher。

watcher是作用域中的内容发生变化时会被通知内容。通过提供两个函数给$watch从而来创建一个watcher:

  • 一个监控函数。监控你所感兴趣的特定的内容
  • 一个监听函数。当数据发生变化的时候被调用

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

硬币的还有一面是$digest函数。

他遍历作用域上的全部的watchers,而且调用他们的watch和listener函数。

为了填充Scope使其满足上面的内容,让我们先定义一个測试用例,假定你能够通过watch来注冊watcher,当调用了digest了以后,listener函数会被触发。

为了让代码更好管理。在scope_spec.jsdescribe块中加入一个嵌套的块。同一时候创建一个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();
});
});
});

在測试案例中我们调用watch在作用域上注冊一个watcher。现在我们对watch函数还不感兴趣,所以我们仅仅返回了一个常量。而listener函数,我们创建了一个JasmineSpy。我们调用digest,然后来检查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函数。

如今我们定义了一个非常easy的版本号。他不过遍历了全部注冊的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来监控字符串。当其变化时数字自增。期望是当第一次调用digest时,counter应该自增,然后每次字符串的值改变,并调用digest后。counter自增。

你应该注意到listener函数的參数发生了变化。和watcher函数一样,他须要scope作为參数,同一时候他也须要watcher的新值和旧值作为參数。

这让开发人员能够更easy的检查属性究竟发生了什么变化。

为了让上述的代码能够工作。$digest须要记住watch函数上一次的值。既然每一个watcher函数我们有了一个对象,我们能够在里面方便的存储上一次的值。例如以下是$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的返回值,即使listenerFn不存在。假设你返回了一个值,该值仍会进行脏值检查。为了保证你使用该模式不产生额外的工作,请不要返回不论什么值。在这样的情况下,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请求,不能保证你的应用究竟能制造多少次请求(即请求的次数不确定??)。

扫一扫。很多其它好文早发现:

构建自己的AngularJS - 作用域和Digest(一)的更多相关文章

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

    作用域 第一章 作用域和Digest(三) $eval - 在当前作用域的上下文中运行代码 Angular有多种方式让你在当前作用域的上下文中运行代码.最简单的是$eval.传入一个函数当做其參数.然 ...

  2. 构建自己的AngularJS,第一部分:作用域和digest 转摘:http://www.ituring.com.cn/article/39865

    构建自己的AngularJS,第一部分:Scope和Digest 原文链接:http://teropa.info/blog/2013/11/03/make-your-own-angular-part- ...

  3. (转)构建自己的AngularJS,第一部分:Scope和Digest

    原翻译链接:https://github.com/xufei/Make-Your-Own-AngularJS/edit/master/01.md 原文链接:http://teropa.info/blo ...

  4. AngularJS开发指南9:AngularJS作用域的详解

    AngularJS作用域是一个指向应用模型的对象.它是表达式的执行环境.作用域有层次结构,这个层次和相应的DOM几乎是一样的.作用域能监控表达式和传递事件. 作用域的特点 作用域提供APIs($wat ...

  5. 深入了解angularjs中的$digest与$apply方法,从区别聊到使用优化

     壹 ❀ 引 如果有人问,在angularjs中修改模型数据为何视图会同步更新呢,我想大多数人一定会回答脏检查(Dirty Checking)相关概念.没错,在angularjs中作用域(scope) ...

  6. AngularJS 作用域(Scope)

    AngularJS作用域(Scope) Scope作用域是应用在视图和控制器之间的纽带,Scope是一个对象包含可用的方法和属性,Scope可以应用在试图和控制器上. $scope是针对当前的cont ...

  7. AngularJS 作用域与数据绑定机制

    AngularJS 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与 ...

  8. 一步步构建自己的AngularJS(2)——scope之$watch及$digest

    在上一节项目初始化中,我们最终得到了一个可以运行的基础代码库,它的基本结构如下: 其中node_modules文件夹存放项目中的第三方依赖模块,src存放我们的项目代码源文件,test存放测试用例文件 ...

  9. 剖析AngularJS作用域

    一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope: ...

随机推荐

  1. sysctl---内核参数相关设置

    sysctl命令被用于在内核运行时动态地修改内核的运行参数,可用的内核参数在目录/proc/sys中.它包含一些TCP/ip堆栈和虚拟内存系统的高级选项, 这可以让有经验的管理员提高引人注目的系统性能 ...

  2. Spring-statemachine fork一个region后不能进入join状态的问题

    Spring-statemachine版本:当前最新的1.2.3.RELEASE版本 发现fork多个Region时,子状态全部完成后能够进入join状态.但是如果fork一个Region时Regio ...

  3. C++刷题——2802: 推断字符串是否为回文

    Description 编敲代码,推断输入的一个字符串是否为回文. 若是则输出"Yes".否则输出"No". 所谓回文是指順读和倒读都是一样的字符串. Inpu ...

  4. 基于Verilog语言的可维护性设计技术

    [注]本文内容主体部分直接翻译参考文献[1]较多内容,因此本文不用于任何商业目的,也不会发表在任何学术刊物上,仅供实验室内部交流和IC设计爱好者交流之用. “曲意而使人喜,不若直节而使人忌:无善而致人 ...

  5. Win form碎知识点

    判断1.ds不能为空 2.ds的表数量必须大于0 3.判断ds的第一个表中的行数必须有 if (ds.Tables.Count > 0 && ds != null &&a ...

  6. VirtualBox内刚刚安装完Debian9系统,也无法设置共享文件夹。解决的方法就是安装VirtualBox客户端增强包。

    VirtualBox内刚刚安装完Debian9系统,也无法设置共享文件夹.解决的方法就是安装VirtualBox客户端增强包. 1.若直接安装客户端增强包会得到如下提示:root@debian:/op ...

  7. 我在看着你呢——shiro学习

    说实话开学第一周效率并不高.项目该结的都差不多结了,看来这毛病我是养成了.项目忙的要死的时候,想休息想停一停就不断往下扔包袱.一下没项目了开学了,反倒开始手痒,捉摸着写点什么代码.马上我的小mac就要 ...

  8. angularCli打包遇到的一些问题

    有时在运行项目或者打包项目的时候会遇到报错信息:found version 4, expected 3, 这个大概意思是说该插件需要的依赖当前不支持,需要提高依赖的版本. 比如:@angular/co ...

  9. strings---对象文件或二进制文件中查找可打印的字符串

    strings命令在对象文件或二进制文件中查找可打印的字符串.字符串是4个或更多可打印字符的任意序列,以换行符或空字符结束. strings命令对识别随机对象文件很有用. 语法 strings [ - ...

  10. 学习——HTML5中事件

    HTML5中新添加了很多事件,但是由于他们的兼容问题不是很理想,应用实战性不是太强,所以在这里基本省略,咱们只分享应用广泛兼容不错的事件,日后随着兼容情况提升以后再陆续添加分享.今天为大家介绍的事件主 ...