>웹 프론트엔드 >JS 튜토리얼 >Angular 데이터 양방향 바인딩 이해_AngularJS

Angular 데이터 양방향 바인딩 이해_AngularJS

WBOY
WBOY원래의
2016-05-16 15:20:351080검색

AngularJS는 많은 Google 제품에서 사용되어 온 뛰어난 프런트엔드 JS 프레임워크입니다. AngularJS에는 많은 기능이 있으며 그 중 가장 핵심은 MVVM, 모듈화, 자동화된 양방향 데이터 바인딩, 의미 태그, 종속성 주입 등입니다.

1. 양방향 데이터 바인딩이란
Angular는 양방향 바인딩 메커니즘을 구현합니다. 소위 양방향 바인딩은 인터페이스의 작업이 실시간으로 데이터에 반영될 수 있고 데이터의 변경 사항이 인터페이스에 실시간으로 표시될 수 있다는 것에 지나지 않습니다.

가장 간단한 예는 다음과 같습니다.

<div ng-controller="CounterCtrl">
  <span ng-bind="counter"></span>
  <button ng-click="counter++">increase</button>
</div>function CounterCtrl($scope) {
  $scope.counter = 1;
}

이 예제는 버튼을 클릭할 때마다 인터페이스의 숫자가 1씩 증가하는 매우 간단합니다.

2. 양방향 데이터 바인딩의 원리
1. 깊은 이해
사용자가 휴대폰 목록 표시 순서를 제어할 수 있습니다. 새로운 모델 속성을 추가하고 이를 반복자와 통합한 후 데이터 바인딩이 나머지 작업을 수행하도록 하여 동적 정렬을 구현할 수 있습니다.

템플릿(app/index.html)

Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
 <option value="name">Alphabetical</option>
 <option value="age">Newest</option>
</select>


<ul class="phones">
 <li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
  {{phone.name}}
  <p>{{phone.snippet}}</p>
 </li>
</ul>

index.html에서 다음 변경 사항이 적용되었습니다.

먼저 사용자가 제공된 두 가지 정렬 방법 중 하나를 선택할 수 있도록 orderProp이라는 221f08282418e2996498697df914ce4e 태그가 추가됩니다.

그런 다음 필터 필터 뒤에 orderBy 필터를 추가하여 반복기에 입력되는 데이터를 처리합니다. orderBy 필터는 배열을 입력으로 사용하여 복사본을 만든 다음 반복기로 출력하기 전에 복사본의 순서를 변경합니다.
AngularJS는 select 요소와 orderProp 모델 사이에 양방향 바인딩을 생성합니다. 그런 다음 orderProp은 orderBy 필터의 입력으로 사용됩니다.

데이터 모델이 변경되면(예: 사용자가 드롭다운 메뉴에서 다른 순서를 선택하는 경우) AngularJS의 데이터 바인딩이 자동으로 뷰를 업데이트합니다. 서투른 DOM 조작이 없습니다.

컨트롤러(app/js/controllers.js)

function PhoneListCtrl($scope) {
 $scope.phones = [
  {"name": "Nexus S",
   "snippet": "Fast just got faster with Nexus S.",
   "age": 0},
  {"name": "Motorola XOOM&#8482; with Wi-Fi",
   "snippet": "The Next, Next Generation tablet.",
   "age": 1},
  {"name": "MOTOROLA XOOM&#8482;",
   "snippet": "The Next, Next Generation tablet.",
   "age": 2}
 ];

 $scope.orderProp = 'age';
}

修改了phones模型—— 手机的数组 ——为每一个手机记录其增加了一个age属性。根据age属性来对手机进行排序。
在控制器代码里加了一行让orderProp的默认值为age。如果我们不设置默认值,这个模型会在用户在下拉菜单选择一个顺序之前一直处于未初始化状态。

现在我们该好好谈谈双向数据绑定了。注意到当应用在浏览器中加载时,“Newest”在下拉菜单中被选中。这是因为我们在控制器中把orderProp设置成了‘age'。所以绑定在从我们模型到用户界面的方向上起作用——即数据从模型到视图的绑定。现在当你在下拉菜单中选择“Alphabetically”,数据模型会被同时更新,并且手机列表数组会被重新排序。这个时候数据绑定从另一个方向产生了作用——即数据从视图到模型的绑定。

2.原理分析
下面的原理想法实际上很基础,可以被认为是3步走计划:

  • 我们需要一个UI元素和属性相互绑定的方法
  • 我们需要监视属性和UI元素的变化
  • 我们需要让所有绑定的对象和元素都能感知到变化

还是有很多方法能够实现上面的想法,有一个简单有效的方法就是使用PubSub模式。 这个思路很简单:我们使用数据特性来为HTML代码进行绑定,所有被绑定在一起的JavaScript对象和DOM元素都会订阅一个PubSub对象。只要JavaScript对象或者一个HTML输入元素监听到数据的变化时,就会触发绑定到PubSub对象上的事件,从而其他绑定的对象和元素都会做出相应的变化。

3.发布者-订阅者模式(PubSub模式)
设计该模式背后的主要动力是促进形成松散耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称为观察者,而补观察的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者并且可能经常以事件对象的形式传递消息。

假设有一个发布者paper,它每天出版报纸及月刊杂志。订阅者joe将被通知任何时候所发生的新闻。

该paper对象需要一个subscribers属性,该属性是一个存储所有订阅者的数组。订阅行为只是将其加入到这个数组中。当一个事件发生时,paper将会循环遍历订阅者列表并通知它们。通知意味着调用订阅者对象的某个方法。故当用户订阅信息时,该订阅者需要向paper的subscribe()提供它的其中一个方法。

paper也提供了unsubscribe()方法,该方法表示从订阅者数组(即subscribers属性)中删除订阅者。paper最后一个重要的方法是publish(),它会调用这些订阅者的方法,总而言之,发布者对象paper需要具有以下这些成员:

  • ①subscribers 一个数组
  • ②subscribe() 将订阅者添加到subscribers数组中
  • ③unsubscribe() 从subscribers数组中删除订阅者
  • ④publish() 循环遍历subscribers数组中的每一个元素,并且调用他们注册时所提供的方法

所有这三种方法都需要一个type参数,因为发布者可能触发多个事件(比如同时发布一本杂志和一份报纸)而用户可能仅选择订阅其中一种,而不是另外一种。

由于这些成员对于任何发布者对象都是通用的,故将它们作为独立对象的一个部分来实现是很有意义的。那样我们可将其复制到任何对象中,并将任意给定对象变成一个发布者。

三.用jQuery做一个简单的实现
对于DOM事件的订阅和发布,用jQuery实现起来是非常简单的,接下来我们就是用Jquery比如下面:

function DataBinder( object_id ) {
 // Use a jQuery object as simple PubSub
 var pubSub = jQuery({});
 
 // We expect a `data` element specifying the binding
 // in the form: data-bind-<object_id>="<property_name>"
 var data_attr = "bind-" + object_id,
   message = object_id + ":change";
 
 // Listen to change events on elements with the data-binding attribute and proxy
 // them to the PubSub, so that the change is "broadcasted" to all connected objects
 jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
  var $input = jQuery( this );
 
  pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
 });
 
 // PubSub propagates changes to all bound elements, setting value of
 // input tags or HTML content of other tags
 pubSub.on( message, function( evt, prop_name, new_val ) {
  jQuery( "[data-" + data_attr + "=" + prop_name + "]" ).each( function() {
   var $bound = jQuery( this );
 
   if ( $bound.is("input, textarea, select") ) {
    $bound.val( new_val );
   } else {
    $bound.html( new_val );
   }
  });
 });
 
 return pubSub;
}

对于上面这个实现来说,下面是一个User模型的最简单的实现方法:

function User( uid ) {
 var binder = new DataBinder( uid ),
 
   user = {
    attributes: {},
 
    // The attribute setter publish changes using the DataBinder PubSub
    set: function( attr_name, val ) {
     this.attributes[ attr_name ] = val;
     binder.trigger( uid + ":change", [ attr_name, val, this ] );
    },
 
    get: function( attr_name ) {
     return this.attributes[ attr_name ];
    },
 
    _binder: binder
   };
 
 // Subscribe to the PubSub
 binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
  if ( initiator !== user ) {
   user.set( attr_name, new_val );
  }
 });
 
 return user;
}

现在我们如果想要将User模型属性绑定到UI上,我们只需要将适合的数据特性绑定到对应的HTML元素上。

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );
 
// html
<input type="number" data-bind-123="name" />

这样输入值会自动映射到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,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 }}
<button ng-click="changeFoo()">Change the name</button>

这里我们有一个$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);
 });
&#65532;});

这个测试用来创建一个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数据双向绑定的相关介绍,希望对大家的学习有所帮助。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.