UDN-企业互联网技术人气社区

板块导航

浏览  : 601
回复  : 5

[AngularJS] 数据的双向绑定 Angular JS

[复制链接]
htmlman的头像 楼主
发表于 2017-1-10 15:02:40 | 显示全部楼层 |阅读模式
  接触AngularJS许了,时常问自己一些问题,如果是我实现它,会在哪些方面选择跟它相同的道路,哪些方面不同。为此,记录了一些思考,给自己回顾,也供他人参考。

  初步大致有以下几个方面:

  • 数据双向绑定
  • 视图模型的继承关系
  • 模块和依赖注入的设计

  数据的双向绑定

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

  一个最简单的示例就是这样:
  1. <div ng-controller="CounterCtrl">
  2.     <span ng-bind="counter"></span>
  3.     <button ng-click="counter=counter+1">increase</button>
  4. </div>
  5. function CounterCtrl($scope) {
  6.     $scope.counter = 1;
  7. }
复制代码

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

  绑定数据是怎样生效的

  初学AngularJS的人可能会踩到这样的坑,假设有一个指令:
  1. var app = angular.module("test", []);

  2. app.directive("myclick", function() {
  3.     return function (scope, element, attr) {
  4.         element.on("click", function() {
  5.             scope.counter++;
  6.         });
  7.     };
  8. });

  9. app.controller("CounterCtrl", function($scope) {
  10.     $scope.counter = 0;
  11. });
  12. <body ng-app="test">
  13.     <div ng-controller="CounterCtrl">
  14.         <button myclick>increase</button>
  15.         <span ng-bind="counter"></span>
  16.     </div>
  17. </body>
复制代码

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

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

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

  我们先想想,假如没有AngularJS,我们想要自己实现这么个功能,应该怎样?
  1. <!DOCTYPE html>
  2. <html>
  3.     <head>
  4.         <meta charset="utf-8" />
  5.         <title>two-way binding</title>
  6.     </head>
  7.     <body onload="init()">
  8.         <button ng-click="inc">
  9.             increase 1
  10.         </button>
  11.         <button ng-click="inc2">
  12.             increase 2
  13.         </button>
  14.         <span style="color:red" ng-bind="counter"></span>
  15.         <span style="color:blue" ng-bind="counter"></span>
  16.         <span style="color:green" ng-bind="counter"></span>

  17.         <script type="text/JavaScript">
  18.             /* 数据模型区开始 */
  19.             var counter = 0;

  20.             function inc() {
  21.                 counter++;
  22.             }

  23.             function inc2() {
  24.                 counter+=2;
  25.             }
  26.             /* 数据模型区结束 */

  27.             /* 绑定关系区开始 */
  28.             function init() {
  29.                 bind();
  30.             }

  31.             function bind() {
  32.                 var list = document.querySelectorAll("[ng-click]");
  33.                 for (var i=0; i<list.length; i++) {
  34.                     list[i].onclick = (function(index) {
  35.                         return function() {
  36.                             window[list[index].getAttribute("ng-click")]();
  37.                             apply();
  38.                         };
  39.                     })(i);
  40.                 }
  41.             }

  42.             function apply() {
  43.                 var list = document.querySelectorAll("[ng-bind='counter']");
  44.                 for (var i=0; i<list.length; i++) {
  45.                     list[i].innerHTML = counter;
  46.                 }
  47.             }
  48.             /* 绑定关系区结束 */
  49.         </script>
  50.     </body>
  51. </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. var app = angular.module("test", []);

  2. app.directive("myclick", function() {
  3.     return function (scope, element, attr) {
  4.         element.on("click", function() {
  5.             scope.counter++;
  6.             scope.$apply(function() {
  7.                 scope.counter++;
  8.             });
  9.         });
  10.     };
  11. });

  12. app.controller("CounterCtrl", function($scope) {
  13.     $scope.counter = 0;
  14. });
复制代码

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

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

  对于$digest来说,在父作用域和子作用域上调用是有差别的,但是,对于$apply来说,这两者一样。我们来构造一个特殊的示例:
  1. var app = angular.module("test", []);

  2. app.directive("increasea", function() {
  3.     return function (scope, element, attr) {
  4.         element.on("click", function() {
  5.             scope.a++;
  6.             scope.$digest();
  7.         });
  8.     };
  9. });

  10. app.directive("increaseb", function() {
  11.     return function (scope, element, attr) {
  12.         element.on("click", function() {
  13.             scope.b++;
  14.             scope.$digest();    //这个换成$apply即可
  15.         });
  16.     };
  17. });

  18. app.controller("OuterCtrl", ["$scope", function($scope) {
  19.     $scope.a = 1;

  20.     $scope.$watch("a", function(newVal) {
  21.         console.log("a:" + newVal);
  22.     });

  23.     $scope.$on("test", function(evt) {
  24.         $scope.a++;
  25.     });
  26. }]);

  27. app.controller("InnerCtrl", ["$scope", function($scope) {
  28.     $scope.b = 2;

  29.     $scope.$watch("b", function(newVal) {
  30.         console.log("b:" + newVal);
  31.         $scope.$emit("test", newVal);
  32.     });
  33. }]);
  34. <div ng-app="test">
  35.     <div ng-controller="OuterCtrl">
  36.         <div ng-controller="InnerCtrl">
  37.             <button increaseb>increase b</button>
  38.             <span ng-bind="b"></span>
  39.         </div>
  40.         <button increasea>increase a</button>
  41.         <span ng-bind="a"></span>
  42.     </div>
  43. </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. function TestCtrl($scope) {
  2.     $scope.numOfCheckedItems = 0;

  3.     var list = [];

  4.     for (var i=0; i<10000; i++) {
  5.         list.push({
  6.             index: i,
  7.             checked: false
  8.         });
  9.     }

  10.     $scope.list = list;

  11.     $scope.toggleChecked = function(flag) {
  12.         for (var i=0; i<list.length; i++) {
  13.             list[i].checked = flag;
  14.             $scope.numOfCheckedItems++;
  15.         }
  16.     };
  17. }
复制代码

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

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

相关帖子

发表于 2017-1-10 15:03:13 来自手机 | 显示全部楼层
jq现在应该算是最火的js框架类了吧?确实很强大extjs基于Yahoo UI的扩展,界面布局和效果方面很给力,但就是有点复杂
使用道具 举报

回复

发表于 2017-1-10 15:20:09 | 显示全部楼层
学习
使用道具 举报

回复

发表于 2017-1-11 03:12:04 | 显示全部楼层
js太强大了,好多工作前端都可以做了…
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部