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


Posted in Javascript onMarch 04, 2016

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中:

<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中:

// 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的人可能会踩到这样的坑,假设有一个指令:

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,我们想要自己实现这么个功能,应该怎样?

<!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框架的代码时,可以把代码写在这个里面调用。

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来说,这两者一样。我们来构造一个特殊的示例:

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框架里,考虑到这样的场景:

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这样把批量操作延时到一次更新,否则性能会更低。

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

Javascript 相关文章推荐
js实现类似新浪微博首页内容渐显效果的方法
Apr 10 Javascript
javascript实现网页字符定位的方法
Jul 14 Javascript
easyui window refresh 刷新两次的解决方法(推荐)
May 18 Javascript
原生js和css实现图片轮播效果
Feb 07 Javascript
Bootstrap进度条学习使用
Feb 09 Javascript
原生js实现可拖拽效果
Feb 28 Javascript
详解AngularJS1.x学习directive 中‘&amp; ’‘=’ ‘@’符号的区别使用
Aug 23 Javascript
详解关于vue2.0工程发布上线操作步骤
Sep 27 Javascript
简单学习5种处理Vue.js异常的方法
Jun 17 Javascript
javascript实现雪花飘落效果
Aug 19 Javascript
全网小程序接口请求封装实例代码
Nov 06 Javascript
JavaScript实现无限轮播效果
Nov 19 Javascript
简单的jQuery banner图片轮播实例代码
Mar 04 #Javascript
百度地图给map添加右键菜单(判断是否为marker)
Mar 04 #Javascript
jquery实现右侧栏菜单选择操作
Mar 04 #Javascript
jQuery实现TAB选项卡切换特效简单演示
Mar 04 #Javascript
jquery实现页面常用的返回顶部效果
Mar 04 #Javascript
JavaScript模拟鼠标右键菜单效果
Dec 08 #Javascript
javascript实现移动端上的触屏拖拽功能
Mar 04 #Javascript
You might like
PHP图片处理之图片背景、画布操作
2014/11/19 PHP
php实现图片以base64显示的方法
2016/10/13 PHP
PHP实现驼峰样式字符串(首字母大写)转换成下划线样式字符串的方法示例
2017/08/10 PHP
PHP实现将几张照片拼接到一起的合成图片功能【便于整体打印输出】
2017/11/14 PHP
javascript 清空form表单中某种元素的值
2009/12/26 Javascript
jQuery数据缓存功能的实现思路及简单模拟
2013/05/27 Javascript
js实现日期级联效果
2014/01/23 Javascript
一个js过滤空格的小函数
2014/10/10 Javascript
JavaScript内存管理介绍
2015/03/13 Javascript
跟我学习javascript的函数调用和构造函数调用
2015/11/16 Javascript
jQuery基于toggle实现click触发DIV的显示与隐藏问题分析
2016/06/12 Javascript
js 上传文件预览的简单实例
2016/08/16 Javascript
从零学习node.js之mysql数据库的操作(五)
2017/02/24 Javascript
浅谈angular2的http请求返回结果的subcribe注意事项
2017/03/01 Javascript
基于JavaScript实现的插入排序算法分析
2017/04/14 Javascript
jquery easyui如何实现格式化列
2017/07/30 jQuery
Js判断H5上下滑动方向及滑动到顶部和底部判断的示例代码
2017/11/15 Javascript
Vue表单及表单绑定方法
2018/09/04 Javascript
JavaScript文本特效实例小结【3个示例】
2018/12/22 Javascript
layer iframe 设置关闭按钮的方法
2019/09/12 Javascript
[19:54]夜魇凡尔赛茶话会 第一期02:看图识人
2021/03/11 DOTA
深入理解Python分布式爬虫原理
2017/11/23 Python
pyqt5利用pyqtDesigner实现登录界面
2019/03/28 Python
python爬取微博评论的实例讲解
2021/01/15 Python
HTML5中新标签和常用标签详解
2014/03/07 HTML / CSS
微软新西兰官方网站:Microsoft New Zealand
2018/08/17 全球购物
Farfetch澳大利亚官网:Farfetch Australia
2020/04/26 全球购物
学习决心书范文
2014/03/11 职场文书
学生会感恩节活动方案
2014/10/11 职场文书
2015年酒店前台工作总结
2015/04/20 职场文书
个人房屋租赁合同(标准范本)
2019/09/16 职场文书
导游词之张家口
2019/12/13 职场文书
Mysql Innodb存储引擎之索引与算法
2022/02/15 MySQL
GO语言异常处理分析 err接口及defer延迟
2022/04/14 Golang
linux目录管理方法介绍
2022/06/01 Servers
Vue Mint UI mt-swipe的使用方式
2022/06/05 Vue.js