深入学习AngularJS中数据的双向绑定机制

来自:http://www.jb51.net/article/80454.htm

Angular JS (Angular.JS) 是一组用来开发Web页面的框架、模板以及数据绑定和丰富UI组件。它支持整个开发进程,提供web应用的架构,无需进行手工DOM操作。 AngularJS很小,只有60K,兼容主流浏览器,与 jQuery 配合良好。双向数据绑定可能是AngularJS最酷最实用的特性,将MVC的原理展现地淋漓尽致.

AngularJS的工作原理是:HTML模板将会被浏览器解析到DOM中, DOM结构成为AngularJS编译器的输入。AngularJS将会遍历DOM模板, 来生成相应的NG指令,所有的指令都负责针对view(即HTML中的ng-model)来设置数据绑定。因此, NG框架是在DOM加载完成之后, 才开始起作用的.

在html中:

?


1

2

3

4

5

6

7

<body ng-app="ngApp">

 <div ng-controller="ngCtl">

  <label ng-model="myLabel"></label>

  <input type="text" ng-model="myInput" />

  <button ng-model="myButton" ng-click="btnClicked"></button>

 </div>

</body>

在js中:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

// angular app

var app = angular.module("ngApp", [], function(){

 console.log("ng-app : ngApp");

});

// angular controller

app.controller("ngCtl", [ ‘$scope‘, function($scope){

 console.log("ng-controller : ngCtl");

 $scope.myLabel = "text for label";

 $scope.myInput = "text for input";

 $scope.btnClicked = function() {

  console.log("Label is " + $scope.myLabel);

 }

}]);

如上,我们在html中先定义一个angular的app,指定一个angular的controller,则该controller会对应于一个作用域(可以用$scope前缀来指定作用域中的属性和方法等). 则在该ngCtl的作用域内的HTML标签, 其值或者操作都可以通过$scope的方式跟js中的属性和方法进行绑定.

这样, 就实现了NG的双向数据绑定: 即HTML中呈现的view与AngularJS中的数据是一致的. 修改其一, 则对应的另一端也会相应地发生变化.

这样的方式,使用起来真的非常方便. 我们仅关心HTML标签的样式, 及其对应在js中angular controller作用域下绑定的属性和方法. 仅此而已, 将众多复杂的DOM操作全都省略掉了.

这样的思想,其实跟jQuery的DOM查询和操作是完全不一样的, 因此也有很多人建议用AngularJS的时候,不要混合使用jQuery. 当然, 二者各有优劣, 使用哪个就要看自己的选择了.

NG中的app相当于一个模块module, 在每个app中可以定义多个controller, 每个controller都会有各自的作用域空间,不会相互干扰.

绑定数据是怎样生效的
初学AngularJS的人可能会踩到这样的坑,假设有一个指令:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

var app = angular.module("test", []);

app.directive("myclick", function() {

  return function (scope, element, attr) {

    element.on("click", function() {

      scope.counter++;

    });

  };

});

app.controller("CounterCtrl", function($scope) {

  $scope.counter = 0;

});

<body ng-app="test">

  <div ng-controller="CounterCtrl">

    <button myclick>increase</button>

    <span ng-bind="counter"></span>

  </div>

</body>

这个时候,点击按钮,界面上的数字并不会增加。很多人会感到迷惑,因为他查看调试器,发现数据确实已经增加了,Angular不是双向绑定吗,为什么数据变化了,界面没有跟着刷新?

试试在scope.counter++;这句之后加一句scope.digest();再看看是不是好了?

为什么要这么做呢,什么情况下要这么做呢?我们发现第一个例子中并没有digest,而且,如果你写了digest,它还会抛出异常,说正在做其他的digest,这是怎么回事?

我们先想想,假如没有AngularJS,我们想要自己实现这么个功能,应该怎样?

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

<!DOCTYPE html>

<html>

  <head>

    <meta charset="utf-8" />

    <title>two-way binding</title>

  </head>

  <body onload="init()">

    <button ng-click="inc">

      increase 1

    </button>

    <button ng-click="inc2">

      increase 2

    </button>

    <span style="color:red" ng-bind="counter"></span>

    <span style="color:blue" ng-bind="counter"></span>

    <span style="color:green" ng-bind="counter"></span>

    <script type="text/javascript">

      /* 数据模型区开始 */

      var counter = 0;

      function inc() {

        counter++;

      }

      function inc2() {

        counter+=2;

      }

      /* 数据模型区结束 */

      /* 绑定关系区开始 */

      function init() {

        bind();

      }

      function bind() {

        var list = document.querySelectorAll("[ng-click]");

        for (var i=0; i<list.length; i++) {

          list[i].onclick = (function(index) {

            return function() {

              window[list[index].getAttribute("ng-click")]();

              apply();

            };

          })(i);

        }

      }

      function apply() {

        var list = document.querySelectorAll("[ng-bind=‘counter‘]");

        for (var i=0; i<list.length; i++) {

          list[i].innerHTML = counter;

        }

      }

      /* 绑定关系区结束 */

    </script>

  </body>

</html>

可以看到,在这么一个简单的例子中,我们做了一些双向绑定的事情。从两个按钮的点击到数据的变更,这个很好理解,但我们没有直接使用DOM的onclick方法,而是搞了一个ng-click,然后在bind里面把这个ng-click对应的函数拿出来,绑定到onclick的事件处理函数中。为什么要这样呢?因为数据虽然变更了,但是还没有往界面上填充,我们需要在此做一些附加操作。

从另外一个方面看,当数据变更的时候,需要把这个变更应用到界面上,也就是那三个span里。但由于Angular使用的是脏检测,意味着当改变数据之后,你自己要做一些事情来触发脏检测,然后再应用到这个数据对应的DOM元素上。问题就在于,怎样触发脏检测?什么时候触发?

我们知道,一些基于setter的框架,它可以在给数据设值的时候,对DOM元素上的绑定变量作重新赋值。脏检测的机制没有这个阶段,它没有任何途径在数据变更之后立即得到通知,所以只能在每个事件入口中手动调用apply(),把数据的变更应用到界面上。在真正的Angular实现中,这里先进行脏检测,确定数据有变化了,然后才对界面设值。

所以,我们在ng-click里面封装真正的click,最重要的作用是为了在之后追加一次apply(),把数据的变更应用到界面上去。

那么,为什么在ng-click里面调用$digest的话,会报错呢?因为Angular的设计,同一时间只允许一个$digest运行,而ng-click这种内置指令已经触发了$digest,当前的还没有走完,所以就出错了。

$digest和$apply
在Angular中,有$apply和$digest两个函数,我们刚才是通过$digest来让这个数据应用到界面上。但这个时候,也可以不用$digest,而是使用$apply,效果是一样的,那么,它们的差异是什么呢?

最直接的差异是,$apply可以带参数,它可以接受一个函数,然后在应用数据之后,调用这个函数。所以,一般在集成非Angular框架的代码时,可以把代码写在这个里面调用。

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

var app = angular.module("test", []);

app.directive("myclick", function() {

  return function (scope, element, attr) {

    element.on("click", function() {

      scope.counter++;

      scope.$apply(function() {

        scope.counter++;

      });

    });

  };

});

app.controller("CounterCtrl", function($scope) {

  $scope.counter = 0;

});

除此之外,还有别的区别吗?

在简单的数据模型中,这两者没有本质差别,但是当有层次结构的时候,就不一样了。考虑到有两层作用域,我们可以在父作用域上调用这两个函数,也可以在子作用域上调用,这个时候就能看到差别了。

对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

var app = angular.module("test", []);

app.directive("increasea", function() {

  return function (scope, element, attr) {

    element.on("click", function() {

      scope.a++;

      scope.$digest();

    });

  };

});

app.directive("increaseb", function() {

  return function (scope, element, attr) {

    element.on("click", function() {

      scope.b++;

      scope.$digest();  //这个换成$apply即可

    });

  };

});

app.controller("OuterCtrl", ["$scope", function($scope) {

  $scope.a = 1;

  $scope.$watch("a", function(newVal) {

    console.log("a:" + newVal);

  });

  $scope.$on("test", function(evt) {

    $scope.a++;

  });

}]);

app.controller("InnerCtrl", ["$scope", function($scope) {

  $scope.b = 2;

  $scope.$watch("b", function(newVal) {

    console.log("b:" + newVal);

    $scope.$emit("test", newVal);

  });

}]);

<div ng-app="test">

  <div ng-controller="OuterCtrl">

    <div ng-controller="InnerCtrl">

      <button increaseb>increase b</button>

      <span ng-bind="b"></span>

    </div>

    <button increasea>increase a</button>

    <span ng-bind="a"></span>

  </div>

</div>

这时候,我们就能看出差别了,在increase b按钮上点击,这时候,a跟b的值其实都已经变化了,但是界面上的a没有更新,直到点击一次increase a,这时候刚才对a的累加才会一次更新上来。怎么解决这个问题呢?只需在increaseb这个指令的实现中,把$digest换成$apply即可。

当调用$digest的时候,只触发当前作用域和它的子作用域上的监控,但是当调用$apply的时候,会触发作用域树上的所有监控。

因此,从性能上讲,如果能确定自己作的这个数据变更所造成的影响范围,应当尽量调用$digest,只有当无法精确知道数据变更造成的影响范围时,才去用$apply,很暴力地遍历整个作用域树,调用其中所有的监控。

从另外一个角度,我们也可以看到,为什么调用外部框架的时候,是推荐放在$apply中,因为只有这个地方才是对所有数据变更都应用的地方,如果用$digest,有可能临时丢失数据变更。

脏检测的利弊
很多人对Angular的脏检测机制感到不屑,推崇基于setter,getter的观测机制,在我看来,这只是同一个事情的不同实现方式,并没有谁完全胜过谁,两者是各有优劣的。

大家都知道,在循环中批量添加DOM元素的时候,会推荐使用DocumentFragment,为什么呢,因为如果每次都对DOM产生变更,它都要修改DOM树的结构,性能影响大,如果我们能先在文档碎片中把DOM结构创建好,然后整体添加到主文档中,这个DOM树的变更就会一次完成,性能会提高很多。

同理,在Angular框架里,考虑到这样的场景:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

function TestCtrl($scope) {

  $scope.numOfCheckedItems = 0;

  var list = [];

  for (var i=0; i<10000; i++) {

    list.push({

      index: i,

      checked: false

    });

  }

  $scope.list = list;

  $scope.toggleChecked = function(flag) {

    for (var i=0; i<list.length; i++) {

      list[i].checked = flag;

      $scope.numOfCheckedItems++;

    }

  };

}

如果界面上某个文本绑定这个numOfCheckedItems,会怎样?在脏检测的机制下,这个过程毫无压力,一次做完所有数据变更,然后整体应用到界面上。这时候,基于setter的机制就惨了,除非它也是像Angular这样把批量操作延时到一次更新,否则性能会更低。

所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。

时间: 2024-12-22 07:43:55

深入学习AngularJS中数据的双向绑定机制的相关文章

AngularJS学习--- AngularJS中数据双向绑定(two-way data-binding) orderBy step4

1.切换工作目录 git checkout step-4 #切换分支,切换到第4步 npm start #启动项目 2.代码 app/index.html Search: <input ng-model="query"> Sort by: <select ng-model="orderProp"> <option value="name">Alphabetical</option> <opti

vue中如何实现数据的双向绑定

vue中如何实现数据的双向绑定 实现视图变化数据跟着变:分两步,上面get中的为第二步(即再次读取的时候会调用get方法得到之前设置的值,以此来实现动态改变) 由于直接写obj.name = this.value;会导致循环调用set方法,所以要借助中间对象的形式把值赋给中间对象,获取obj.name的时候我们获取中间对象的最新值即可 let obj = {name:'zhufeng',age:9};//数据 let temp = {name:"lily"};//借助中间对象 let

(八)简单了解下angularJS框架中NB的双向数据绑定机制,大大减少需要重复的开发代码量

之前写的第一篇angularJS入门文章 ,介绍ng-model的时候提到:使用angularJS的双向数据绑定机制,不需要我们编写繁琐的代码来实现同样的功能.现在我们看一个比较震撼的例子,看看angularJS是如何减少我们在前端开发中的繁琐劳动的.越是感受到框架功能的强大,越是能够激发学习的兴趣和动力. 假如我们有一个学生信息列表,包含学生的姓名.地址和年龄信息.假如这个数据源信息保存在data.js文件中. var g_phones = [ <span style="white-sp

数据的双向绑定 Angular JS

接触AngularJS许了,时常问自己一些问题,如果是我实现它,会在哪些方面选择跟它相同的道路,哪些方面不同.为此,记录了一些思考,给自己回顾,也供他人参考. 初步大致有以下几个方面: 数据双向绑定 视图模型的继承关系 模块和依赖注入的设计 待定 数据的双向绑定 Angular实现了双向绑定机制.所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面. 一个最简单的示例就是这样: <div ng-controller="CounterCtrl"> &

Angular数据双向绑定机制

数据的双向绑定 Angular实现了双向绑定机制.所谓的双向绑定,无非是从界面的操作能实时反映到数据,数据的变更能实时展现到界面. 一个最简单的示例就是这样: <div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</butto

ng1中 如何用双向绑定 实现单向绑定的初始时不显示双括号效果?

ng1中 如何用双向绑定 实现单向绑定(ng-bind就可以不显示{{}})的初始时不显示双括号效果? AngularJS 实例 页面加载时防止应用闪烁: <div ng-app=""> <p ng-cloak>{{ 5 + 5 }}</p> </div> 尝试一下 » 定义和用法 ng-cloak 指令用于在 AngularJS 应用在加载时防止 AngularJS 代码未加载完而出现的问题. AngularJS 应用在加载时,文档可能

JAVA学习篇--ThreadLocal,Java中特殊的线程绑定机制

在DRP项目中,我们使用了ThreadLocal来创建Connection连接,避免了一直以参数的形式将Connection向下传递(传递connection的目的是由于jdbc事务要求确保使用同一个connection连接).那么ThreadLocal是如果做到的呢?它和同步锁的不同在哪里? 是什么: 对于ThreadLocal看英文单词我们很容易理解为一个线程的本地实现,但是它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLoc

AngularJS学习--- AngularJS中模板链接和图像 ng-src step6

接上一篇文章,本文将主要介绍angularjs中的模板链接,和图像显示? 首先,切换分支,启动项目: git checkout step-6 npm start 1.效果 相较于前一篇文章,明显感觉多了图片,那么这些图片是怎么加载进去的呢?这里图片各不一样,如果用传统的方式去加载图片可能要写很多代码,这里看一下angularjs是如何实现的?? 2.实现代码 app/index.html <ul class="phones"> <li ng-repeat="

AngularJS学习--- AngularJS中XHR(AJAX)和依赖注入(DI) step5

前言:本文接前一篇文章,主要介绍什么是XHR,AJAX,DI,angularjs中如何使用XHR和DI. 1.切换工具目录 git checkout -f step-5 #切换分支 npm start #启动项目 2.什么是XHR和依赖注入(Dependency Injection)? 1)什么是XHR? XHR是XMLHttpRequest的简称,XMLHttpRequest 用于在后台与服务器交换数据,主要是为了实现在不重新加载整个网页的情况下,对网页的某部分进行更新.简单说,浏览器中URL