Angular JS數(shù)據(jù)的雙向綁定詳解及實(shí)例
Angular JS數(shù)據(jù)的雙向綁定
接觸AngularJS許了,時(shí)常問自己一些問題,如果是我實(shí)現(xiàn)它,會在哪些方面選擇跟它相同的道路,哪些方面不同。為此,記錄了一些思考,給自己回顧,也供他人參考。
初步大致有以下幾個(gè)方面:
- 數(shù)據(jù)雙向綁定
- 視圖模型的繼承關(guān)系
- 模塊和依賴注入的設(shè)計(jì)
- 待定
數(shù)據(jù)的雙向綁定
Angular實(shí)現(xiàn)了雙向綁定機(jī)制。所謂的雙向綁定,無非是從界面的操作能實(shí)時(shí)反映到數(shù)據(jù),數(shù)據(jù)的變更能實(shí)時(shí)展現(xiàn)到界面。
一個(gè)最簡單的示例就是這樣:
<div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</button> </div> function CounterCtrl($scope) { $scope.counter = 1; }
這個(gè)例子很簡單,毫無特別之處,每當(dāng)點(diǎn)擊一次按鈕,界面上的數(shù)字就增加一。
綁定數(shù)據(jù)是怎樣生效的
初學(xué)AngularJS的人可能會踩到這樣的坑,假設(shè)有一個(gè)指令:
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>
這個(gè)時(shí)候,點(diǎn)擊按鈕,界面上的數(shù)字并不會增加。很多人會感到迷惑,因?yàn)樗榭凑{(diào)試器,發(fā)現(xiàn)數(shù)據(jù)確實(shí)已經(jīng)增加了,Angular不是雙向綁定嗎,為什么數(shù)據(jù)變化了,界面沒有跟著刷新?
試試在scope.counter++;這句之后加一句scope.digest();再看看是不是好了?
為什么要這么做呢,什么情況下要這么做呢?我們發(fā)現(xiàn)第一個(gè)例子中并沒有digest,而且,如果你寫了digest,它還會拋出異常,說正在做其他的digest,這是怎么回事?
我們先想想,假如沒有AngularJS,我們想要自己實(shí)現(xiàn)這么個(gè)功能,應(yīng)該怎樣?
<!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"> /* 數(shù)據(jù)模型區(qū)開始 */ var counter = 0; function inc() { counter++; } function inc2() { counter+=2; } /* 數(shù)據(jù)模型區(qū)結(jié)束 */ /* 綁定關(guān)系區(qū)開始 */ 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; } } /* 綁定關(guān)系區(qū)結(jié)束 */ </script> </body> </html>
可以看到,在這么一個(gè)簡單的例子中,我們做了一些雙向綁定的事情。從兩個(gè)按鈕的點(diǎn)擊到數(shù)據(jù)的變更,這個(gè)很好理解,但我們沒有直接使用DOM的onclick方法,而是搞了一個(gè)ng-click,然后在bind里面把這個(gè)ng-click對應(yīng)的函數(shù)拿出來,綁定到onclick的事件處理函數(shù)中。為什么要這樣呢?因?yàn)閿?shù)據(jù)雖然變更了,但是還沒有往界面上填充,我們需要在此做一些附加操作。
從另外一個(gè)方面看,當(dāng)數(shù)據(jù)變更的時(shí)候,需要把這個(gè)變更應(yīng)用到界面上,也就是那三個(gè)span里。但由于Angular使用的是臟檢測,意味著當(dāng)改變數(shù)據(jù)之后,你自己要做一些事情來觸發(fā)臟檢測,然后再應(yīng)用到這個(gè)數(shù)據(jù)對應(yīng)的DOM元素上。問題就在于,怎樣觸發(fā)臟檢測?什么時(shí)候觸發(fā)?
我們知道,一些基于setter的框架,它可以在給數(shù)據(jù)設(shè)值的時(shí)候,對DOM元素上的綁定變量作重新賦值。臟檢測的機(jī)制沒有這個(gè)階段,它沒有任何途徑在數(shù)據(jù)變更之后立即得到通知,所以只能在每個(gè)事件入口中手動調(diào)用apply(),把數(shù)據(jù)的變更應(yīng)用到界面上。在真正的Angular實(shí)現(xiàn)中,這里先進(jìn)行臟檢測,確定數(shù)據(jù)有變化了,然后才對界面設(shè)值。
所以,我們在ng-click里面封裝真正的click,最重要的作用是為了在之后追加一次apply(),把數(shù)據(jù)的變更應(yīng)用到界面上去。
那么,為什么在ng-click里面調(diào)用$digest的話,會報(bào)錯(cuò)呢?因?yàn)锳ngular的設(shè)計(jì),同一時(shí)間只允許一個(gè)$digest運(yùn)行,而ng-click這種內(nèi)置指令已經(jīng)觸發(fā)了$digest,當(dāng)前的還沒有走完,所以就出錯(cuò)了。
$digest和$apply
在Angular中,有$apply和$digest兩個(gè)函數(shù),我們剛才是通過$digest來讓這個(gè)數(shù)據(jù)應(yīng)用到界面上。但這個(gè)時(shí)候,也可以不用$digest,而是使用$apply,效果是一樣的,那么,它們的差異是什么呢?
最直接的差異是,$apply可以帶參數(shù),它可以接受一個(gè)函數(shù),然后在應(yīng)用數(shù)據(jù)之后,調(diào)用這個(gè)函數(shù)。所以,一般在集成非Angular框架的代碼時(shí),可以把代碼寫在這個(gè)里面調(diào)用。
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; });
除此之外,還有別的區(qū)別嗎?
在簡單的數(shù)據(jù)模型中,這兩者沒有本質(zhì)差別,但是當(dāng)有層次結(jié)構(gòu)的時(shí)候,就不一樣了。考慮到有兩層作用域,我們可以在父作用域上調(diào)用這兩個(gè)函數(shù),也可以在子作用域上調(diào)用,這個(gè)時(shí)候就能看到差別了。
對于$digest來說,在父作用域和子作用域上調(diào)用是有差別的,但是,對于$apply來說,這兩者一樣。我們來構(gòu)造一個(gè)特殊的示例:
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(); //這個(gè)換成$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>
這時(shí)候,我們就能看出差別了,在increase b按鈕上點(diǎn)擊,這時(shí)候,a跟b的值其實(shí)都已經(jīng)變化了,但是界面上的a沒有更新,直到點(diǎn)擊一次increase a,這時(shí)候剛才對a的累加才會一次更新上來。怎么解決這個(gè)問題呢?只需在increaseb這個(gè)指令的實(shí)現(xiàn)中,把$digest換成$apply即可。
當(dāng)調(diào)用$digest的時(shí)候,只觸發(fā)當(dāng)前作用域和它的子作用域上的監(jiān)控,但是當(dāng)調(diào)用$apply的時(shí)候,會觸發(fā)作用域樹上的所有監(jiān)控。
因此,從性能上講,如果能確定自己作的這個(gè)數(shù)據(jù)變更所造成的影響范圍,應(yīng)當(dāng)盡量調(diào)用$digest,只有當(dāng)無法精確知道數(shù)據(jù)變更造成的影響范圍時(shí),才去用$apply,很暴力地遍歷整個(gè)作用域樹,調(diào)用其中所有的監(jiān)控。
從另外一個(gè)角度,我們也可以看到,為什么調(diào)用外部框架的時(shí)候,是推薦放在$apply中,因?yàn)橹挥羞@個(gè)地方才是對所有數(shù)據(jù)變更都應(yīng)用的地方,如果用$digest,有可能臨時(shí)丟失數(shù)據(jù)變更。
臟檢測的利弊
很多人對Angular的臟檢測機(jī)制感到不屑,推崇基于setter,getter的觀測機(jī)制,在我看來,這只是同一個(gè)事情的不同實(shí)現(xiàn)方式,并沒有誰完全勝過誰,兩者是各有優(yōu)劣的。
大家都知道,在循環(huán)中批量添加DOM元素的時(shí)候,會推薦使用DocumentFragment,為什么呢,因?yàn)槿绻看味紝OM產(chǎn)生變更,它都要修改DOM樹的結(jié)構(gòu),性能影響大,如果我們能先在文檔碎片中把DOM結(jié)構(gòu)創(chuàng)建好,然后整體添加到主文檔中,這個(gè)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++; } }; }
如果界面上某個(gè)文本綁定這個(gè)numOfCheckedItems,會怎樣?在臟檢測的機(jī)制下,這個(gè)過程毫無壓力,一次做完所有數(shù)據(jù)變更,然后整體應(yīng)用到界面上。這時(shí)候,基于setter的機(jī)制就慘了,除非它也是像Angular這樣把批量操作延時(shí)到一次更新,否則性能會更低。
所以說,兩種不同的監(jiān)控方式,各有其優(yōu)缺點(diǎn),最好的辦法是了解各自使用方式的差異,考慮出它們性能的差異所在,在不同的業(yè)務(wù)場景中,避開最容易造成性能瓶頸的用法。
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
相關(guān)文章
AngularJS集合數(shù)據(jù)遍歷顯示的實(shí)例
下面小編就為大家分享一篇AngularJS集合數(shù)據(jù)遍歷顯示的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12angular select 默認(rèn)值設(shè)置方法
下面小編就為大家?guī)硪黄猘ngular select 默認(rèn)值設(shè)置方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Angular4學(xué)習(xí)教程之HTML屬性綁定的方法
這篇文章主要給大家介紹了關(guān)于Angular4學(xué)習(xí)教程之HTML屬性綁定的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01AngularJS轉(zhuǎn)換響應(yīng)內(nèi)容
這篇文章主要介紹了AngularJS轉(zhuǎn)換響應(yīng)內(nèi)容 的相關(guān)資料,需要的朋友可以參考下2016-01-01淺談angular2的http請求返回結(jié)果的subcribe注意事項(xiàng)
下面小編就為大家?guī)硪黄獪\談angular2的http請求返回結(jié)果的subcribe注意事項(xiàng)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03解決Angular.Js與Django標(biāo)簽沖突的方案
AngularJS和django的模板都是用{{}}來引用變量的,這就導(dǎo)致了沖突,所以這篇文章主要就給大家介紹了如何解決Angular.Js與Django標(biāo)簽沖突的方案,有需要的朋友們可以參考借鑒,下面來一起學(xué)習(xí)學(xué)習(xí)吧。2016-12-12解決ng-repeat產(chǎn)生的ng-model中取不到值的問題
今天小編就為大家分享一篇解決ng-repeat產(chǎn)生的ng-model中取不到值的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10AngularJS 雙向數(shù)據(jù)綁定詳解簡單實(shí)例
這篇文章主要介紹了AngularJS 雙向數(shù)據(jù)綁定詳解簡單實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-10-10