理解Angular数据双向绑定

AngularJS是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。

一.什么是数据双向绑定

Angular实现了双向绑定机制。所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面。

一个最简单的示例就是这样:

rush:xhtml;">
function CounterCtrl($scope) { $scope.counter = 1; }

这个例子很简单,每当点击一次按钮,界面上的数字就增加一。

二.数据双向绑定原理

1.深入理解 实现用户控制手机列表显示顺序的特性。动态排序可以这样实现,添加一个新的模型属性,把它和迭代器集成起来,然后让数据绑定完成剩下的事情。

模板(app/index.html)

rush:js;"> Search:

在index.html中做了如下更改:

首先,增加一个叫做orderProp的

这样输入值会自动映射到user对象的name属性,反之亦然。到此这个简单实现就完成了。

四.Angular实现数据双向绑定

Angular主要通过scopes实现数据双向绑定。AngularJS的scopes包括以下四个主要部分:

digest循环以及dirty-checking,包括watch,digest,和$apply。 Scope继承 - 这项机制使得我们可以创建scope继承来分享数据和事件。 对集合 – 数组和对象 – 的有效dirty-checking。 事件系统 - on,emit,以及$broadcast。

我们主要讲解第一条Angular数据绑定是怎么实现的。

1.digest循环以及dirty-checking,包括watch,和$apply ①浏览器事件循环和Angular.js扩展 我们的浏览器一直在等待事件,比如用户交互。假如你点击一个按钮或者在输入框里输入东西,事件的回调函数就会在javascript解释器里执行,然后你就可以做任何DOM操作,等回调函数执行完毕时,浏览器就会相应地对DOM做出变化。 Angular拓展了这个事件循环,生成一个有时成为angular context的执行环境(这是个重要的概念)。

②watch队列(watch list) 每次你绑定一些东西到你的UI上时你就会往$watch队列里插入一条$watch。想象一下$watch就是那个可以检测它监视的model里时候有变化的东西。

当我们的模版加载完毕时,也就是在linking阶段(Angular分为compile阶段和linking阶段---译者注),Angular解释器会寻找每个directive,然后生成每个需要的$watch。

③$digest循环 还记得我前面提到的扩展的事件循环吗?当浏览器接收到可以被angular context处理的事件时,digest循环就会触发。这个循环是由两个更小的循环组合起来的。一个处理evalAsync队列,另一个处理watch队列。 这个是处理什么的呢?digest将会遍历我们的watch,然后询问它是否有属性和值的变化,直$watch队列都检查过。

这就是所谓的dirty-checking。既然所有的$watch都检查完了,那就要问了:有没有$watch更新过?如果有至少一个更新过,这个循环就会再次触发,直到所有的$watch都没有变化。这样就能够保证每个model都已经不会再变化。记住如果循环超过10次的话,它将会抛出一个异常,防止无限循环。 当$digest循环结束时,DOM相应地变化。

例如:controllers.js

rush:js;"> app.controller('MainCtrl',function() { $scope.name = "Foo";

$scope.changeFoo = function() {
$scope.name = "Bar";
}
});

rush:js;"> {{ name }}

这里我们有一个$watch因为ng-click不生成$watch(函数是不会变的)。

  • 一个事件,进入angular context(后面会解释为什么)。

  • 查询每个$watch是否变化。

  • cope.name的$watch报告了变化,它会强制再执行一次$digest循环。

  • cope.name新值相应部分的DOM。

这里很重要的(也是许多人的很蛋疼的地方)是每一个进入angular context的事件都会执行一个$digest循环,也就是说每次我们输入一个字母循环都会检查整个页面的所有$watch。

④通过$apply来进入angular context 谁决定什么事件进入angular context,而哪些又不进入呢?$apply!

如果当事件触发时,你调用apply,它会进入angularcontext,如果没有调用就不会进入。现在你可能会问:刚才的例子里我也没有调用apply,为什么?Angular为你做了!因此你点击带有ng-click的元素时,时间就会被封装到一个apply调用。如果你有一个ng−model="foo"的输入框,然后你敲一个f,事件就会这样调用apply("foo = 'f';")。

Angular什么时候不会自动为我们apply呢?这是Angular新手共同的痛处。为什么我的jQuery不会更新我绑定的东西呢?因为jQuery没有调用apply,事件没有进入angular context,$digest循环永远没有执行。

2.具体实现 AngularJS的scopes就是一般的JavaScript对象,在它上面你可以绑定你喜欢的属性和其他对象,然而,它们同时也被添加了一些功能用于观察数据结构上的变化。这些观察的功能都由dirty-checking来实现并且都在一个digest循环中被执行。 ①Scope 对象 创建一个test/scope_spec.js文件,并将下面的测试代码添加到其中:

rush: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); }); });

这个测试用来创建一个Scope,并在它上面赋一个任意值。我们可以轻松的让这个测试通过:创建src/scope.js文件然后在其中添加以下内容: src/scope.js

rush:js;"> ------ /* jshint globalstrict: true */ 'use strict'; function Scope() { }

在这个测试中,我们将一个属性(aProperty)赋值给了这个scope。这正是Scope上的属性运行的方式。它们就是正常的JavaScript属性,并没有什么特别之处。这里你完全不需要去调用一个特别的setter,也不需要对你赋值的类型进行什么限制。真正的魔法在于两个特别的函数:watch和digest。我们现在就来看看这两个函数

②监视对象属性:watch和digest watch和digest是同一个硬币的两面。它们二者同时形成了$digest循环的核心:对数据的变化做出反应。

为了实现这一块功能,我们首先来定义一个测试文件并断言你可以使用watch来注册一个监视器,并且当有人调用了digest的时候监视器的监听函数会被调用

在scope_spec.js文件添加一个嵌套的describe块。并创建一个beforeEach函数来初始化这个scope,以便我们可以在进行每个测试时重复它:

test/scope_spec.js

rush:js;"> ------ describe("Scope",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来在这个scope上注册一个监视器。我们现在对于监视函数本身并没有什么兴趣,因此我们随便提供了一个函数来返回一个常数值。作为监听函数,我们提供了一个JasmineSpy。接着我们调用了digest并检查这个监听器是否真正被调用

首先,这个Scope需要有一些地方去存储所有被注册的监视器。我们现在就在Scope构造函数添加一个数组存储它们:

src/scope.js

rush:js;"> -----

function Scope(){
this.$$watchers = [];
}

上面代码中的$$前缀在AngularJS框架中被认为是私有变量,它们不应该在应用的外部被调用。 现在我们可以来定义watch函数了。它接收两个函数作为参数,并且将它们储存在$watchers数组中。我们想要每一个Scope对象都拥有这个函数,因此我们将它添加到Scope的原型中:

rush:js;"> src/scope.js ----- Scope.prototype.$watch = function(watchFn,listenerFn) { var watcher = { watchFn: watchFn,listenerFn: listenerFn }; this.$$watchers.unshift(watcher); };

最后我们应该有一个digest函数。现在,我们来定义一个digest函数的简化版本,它仅仅只是会迭代所有的注册监视器并调用它们的监听函数: digest能够持续的迭代所有监视函数,直到被监视的值停止变化。多做几次digest是我们能够获得运用于监视器并依赖于其他监视器的变化。

首先,我们新建名为$$digestOnce,并且调整它以便它能够在所有监视器上运行一遍,然后返回一个布尔值来说明有没有任何变化: src/scope.js

rush:js;"> ----

Scope.prototype.$$digestOnce = function(){
var length = this.$$watchers.length;
var watcher,newValue,oldValue,dirty;
while(length--){
watcher = this.$$watchers[length];
newValue = watcher.watchFn(this);
oldValue= watcher.last;
if(newValue !== oldValue){
watcher.last == newValue;
watcher.listenerFn(newValue,this);
dirty = true;
}
}
return dirty;
};

接着,我们重定义digest以便它能够运行“外循环”,在变化发生时调用$digestOnce:

src/scope.js

rush:js;"> ----- Scope.prototype.$digest = function(){ var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };

以上就是Angular数据双向绑定的相关介绍,希望对大家的学习有所帮助。

相关文章

前言 做过web项目开发的人对layer弹层组件肯定不陌生,作为l...
前言 前端表单校验是过滤无效数据、假数据、有毒数据的第一步...
前言 图片上传是web项目常见的需求,我基于之前的博客的代码...
前言 导出Excel文件这个功能,通常都是在后端实现返回前端一...
前言 众所周知,js是单线程的,从上往下,从左往右依次执行,...
前言 项目开发中,我们可能会碰到这样的需求:select标签,禁...