理解Angular数据双向绑定

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

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

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

function CounterCtrl($scope) { $scope.counter = 1; }

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

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

模板(app/index.html)

Search: 
Sort by:



  • {{phone.name}}

    {{phone.snippet}}

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

首先,增加了一个叫做orderProp的

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

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

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

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

1.digest循环以及dirty-checking,包括watch,digest,和$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

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

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

{{ name }}

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

  • 我们按下按钮
  • 浏览器接收到一个事件,进入angular context(后面会解释为什么)。
  • $digest循环开始执行,查询每个$watch是否变化。
  • 由于监视$scope.name的$watch报告了变化,它会强制再执行一次$digest循环。
  • 新的$digest循环没有检测到变化。
  • 浏览器拿回控制权,更新与$scope.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文件,并将下面的测试代码添加到其中:

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 

------
/* 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  

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

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

src/scope.js

-----

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

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

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

----

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, oldValue, this);
      dirty = true;
    }
  }
   return dirty;
};

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

src/scope.js

-----
Scope.prototype.$digest = function(){
  var dirty;
  do {
    dirty = this.$$digestOnce();
  } while (dirty);
};

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

你可能感兴趣的:(理解Angular数据双向绑定)