首頁 >web前端 >js教程 >深入學習AngularJS中資料的雙向綁定機制_AngularJS

深入學習AngularJS中資料的雙向綁定機制_AngularJS

WBOY
WBOY原創
2016-05-16 15:12:041626瀏覽

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這樣把批量操作延時到一次更新,否則效能會更低。

所以說,兩種不同的監控方式,各有其優缺點,最好的辦法是了解各自使用方式的差異,考慮出它們性能的差異所在,在不同的業務場景中,避開最容易造成效能瓶頸的用法。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn