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); } }]);
初學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>
試試在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这样把批量操作延时到一次更新,否则性能会更低。
所以说,两种不同的监控方式,各有其优缺点,最好的办法是了解各自使用方式的差异,考虑出它们性能的差异所在,在不同的业务场景中,避开最容易造成性能瓶颈的用法。
更多深入学习AngularJS中数据的双向绑定机制相关文章请关注PHP中文网!