angularJS 雙向數據綁定、作用域、表達式、


在Rails等傳統Web框架中,控制器將多個模型中的數據和模板組合在一起形成視圖,並將其提供給用戶,這個組合過程會產生一個單向視圖。
AngularJS則采用了完全不同的解決方案。它創建實時模板來代替視圖而不是將數據合並進模板之后更新DOM。任何一個獨立視圖組件中的值都是動態替換的。這個功能可以說是AngularJS中最重要的功能之一

ng-app屬性聲明所有被其包含的內容都屬於這個AngularJS應用,只有被具有ng-app屬性的DOM元素包含的元素才會受AngularJS影響。

AngularJS會記錄數據模型所包含的數據在任何特定時間點的值,

當AngularJS認為某個值可能發生變化時,它會運行自己的事件循環來檢查這個值是否變“臟”。如果該值從上次事件循環運行之后發生了變化,則該值被認為是“臟”值。這也是Angular可以跟蹤和響應應用變化的方式

這個事件循環會調用$digest()循環,這個過程被稱作臟檢查(dirty checking) 。臟檢查是檢查數據模型變化的有效手段, AngularJS會在事件循環時執行臟檢查(第24章會深入討論)來保證數據的一致性。


為了表示內部和內置的庫函數,Angular使用$預定義對象。盡管這類似於全局的jQuery對象$,但它們是完全無關的。只要遇到$符號,你都可以只把它看作一個Angular對象。

簡單的數據綁定

使用ng-model指令將內部數據模型對象($scope)中的name屬性綁定到了文本輸入字段上,無論在文本輸入字段中輸入了什么,都會同步到數據模型中。$scope對象是一個簡單的JavaScript對象,其中的屬性可以被視圖訪問,也可以同控制器進行交互。

<input type="text" ng-model = "name" ><br>
                <div>hello {{name}}</div>

 

DOM 元 素 上 的ng-controller聲明所有被它包含的元素都屬於某個控制器。
一個顯示時間的小例子: 

<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="app.js"></script>

<div ng-app="testmodule">
    <div ng-controller="MyController">
        <span>{{clock}}</span>
    </div>
</div>

app.js  中的內容:

angular.module('testmodule',[])
              .controller("MyController",['$scope',function($scope){
                  $scope.clock = new Date();

                  var up = function(){
                      $scope.clock = new Date();
                  }
                  setInterval(function(){
                      $scope.$apply(up);
                  },1000);

              }]);

 

作用域(scope)

作用域是視圖和控制器之間的膠水。視圖中的模板會和作用域進行連接,應用會對DOM進行設置以便將屬性變化通知給AngularJS。

作用域是應用狀態的基礎。基於動態綁定,可以依賴視圖在修改數據時立刻更新$scope,也可以依賴$scope在其發生變化時立刻重新渲染視圖。

AngularJS將$scope設計成和DOM類似的結構,因此$scope可以進行嵌套,也就是說我們可以引用父級$scope中的屬性。

作用域提供了監視數據模型變化的能力。它允許開發者使用其中的apply機制,將數據模型的變化在整個應用范圍內進行通知。

AngularJS啟動並生成視圖時,會將根ng-app元素同$rootScope進行綁定。 $rootScope是所有$scope對象的最上層。

 $rootScope是AngularJS中最接近全局作用域的對象,可以看作是 根作用域,在$rootScope上附加太多業務邏並不是好主意,這與污染JavaScript的全局作用域是一樣的。

將應用的業務邏輯都放在控制器中,而將相關的數據都放在控制器的作用域中,這是非常完美的架構。

$scope對象就是一個普通的JavaScript對象,我們可以在其上隨意修改或添加屬性。

$scope對象在AngularJS中充當數據模型,$scope並不負責處理和操作數據,

 

作用域能做什么

有以下的基本功能:
 提供觀察者以監視數據模型的變化;
 可以將數據模型的變化通知給整個應用,甚至是系統外的組件;
 可以進行嵌套,隔離業務功能和數據;
 給表達式提供運算時所需的執行環境。

 

$scope 的生命周期

 當Angular關心的事件發生在瀏覽器中時,比如用戶在通過ng-model屬性監控的輸入字段中輸入,或者帶有ng-click屬性的按鈕被點擊時, Angular的事件循環都會啟動
每當事件被處理時, $scope就會對定義的表達式求值。此時事件循環會啟動,並且Angular應用會監控應用程序內的所有對象,臟值檢測循環也會運行。

$scope對象的生命周期處理有四個不同階段。

.1 創建

創建控制器或指令時, AngularJS會用$injector創建一個新的作用域,並在這個新建的控制器或指令運行時將作用域傳遞進去。

.2 鏈接

當Angular開始運行時,所有的$scope對象都會附加或者鏈接到視圖中。所有創建$scope對象的函數也會將自身附加到視圖中。這些作用域將會注冊當Angular應用上下文中發生變化時需要運行的函數。

這些函數被稱為$watch函數, Angular通過這些函數獲知何時啟動事件循環。

.3 更新

當事件循環運行時,它通常執行在頂層$scope對象上(被稱作$rootScope),每個子作用域都執行自己的臟值檢測。每個監控函數都會檢查變化。如果檢測到任意變化, $scope對象就會觸
發指定的回調函數。

.4 銷毀

當一個$scope在視圖中不再需要時,這個作用域將會清理和銷毀自己。

盡管永遠不會需要清理作用域(因為Angular會為你處理),但是知道是誰創建了這個作用域還是有用的,因為你可以使用這個$scope上叫做$destory()的方法來清理這個作用域。

 

 

表達式

{{ }}符號將一個變量綁定到$scope上的寫法本質上就是一個表達式: {{ expression }}。  當用$watch進行監聽時, AngularJS會對表達式或函數進行運算。

表達式和eval(javascript)非常相似,但是由於表達式由AngularJS來處理,它們有以下顯著不同的特性:
 所有的表達式都在其所屬的作用域內部執行,並有訪問本地$scope的權限;
 如果表達式發生了TypeError和ReferenceError並不會拋出異常;
 不允許使用任何流程控制功能(條件控制,例如if/eles);
 可以接受過濾器和過濾器鏈。

 

AngularJS通過$parse【解析】這個內部服務來進行表達式的運算,這個服務能夠訪問當前所處的作用域。這個過程允許我們訪問定義在$scope上的原始JavaScript數據和函數。

將$parse服務注入到控制器中,然后調用它就可以實現手動解析表達式。舉例來說,如果頁面上有一個輸入框綁定到了expr變量上,
這是一個數據雙向綁定的例子,目前還不知道$parse的用法,

<div ng-controller="ParentController">
     <input type="text" ng-model="expr" placeholder="請輸入"><br>
     <div>{{parseValue}}</div>                               
</div>
var app = angular.module("app",[]);
app.controller("ParentController",['$scope','$parse',function($scope,$parse){
                  $scope.$watch('expr',function(newVal,oddVal,scope){
                      if(newVal !== oddVal){
                          // 讓我們建立parseFun表達式,目前 $prase()怎么解析還不知道
                    var parseFun = $parse(newVal);
                  // 獲取記過解析后的表達式的值,放在scope里,output出來
                    scope.parsedExpr = parseFun(scope);
                      }                          
                  });
}]);

 

插值字符串

在AngularJS中,我們的確有手動運行模板編譯的能力,例如,在字符串模板中做插值操作,需要在你的對象中注入$interpolate服務。

$interpolate服務是一個可以接受三個參數的函數,其中第一個參數是必需的。
 text(字符串):一個包含字符插值標記的字符串。
 mustHaveExpression(布爾型):如果將這個參數設為true,當傳入的字符串中不含有表達式時會返回null。
 trustedContext(字符串): AngularJS會對已經進行過字符插值操作的字符串通過$sec.getTrusted()方法進行嚴格的上下文轉義。

 

        <div class="panel panel-primary" ng-app="app">
            <div class="panel-heading">
                <div class="panel-title">雙向數據綁定</div>
            </div>


            <div class="panel-body" ng-controller="ParentController">
                
                <input type="email" ng-model="name" placeholder="請輸入"><br>
                <textarea ng-model="emailbody" ></textarea>
                
                <pre>{{parseValue}}</pre>    
                                    
            </div>

        </div>    
app.js:

var app = angular.module("app",[]);
app.controller("ParentController",function($scope,$interpolate){
                  $scope.$watch('emailbody',function(body){

                      if(body){
                          var tem = $interpolate(body);
                          $scope.parseValue = tem({name : $scope.name});
                      }
                      
                  });
});

效果:

 

AngularJS 的生命周期

總共有兩個主要階段:  編譯階段 和 鏈接階段

編譯階段

第一個階段是編譯階段。在編譯階段, AngularJS會遍歷整個HTML文檔並根據JavaScript中的指令定義來處理頁面上聲明的指令。


每一個指令的模板中都可能含有另外一個指令,另外一個指令也可能會有自己的模板。當AngularJS調用HTML文檔根部的指令時,會遍歷其中所有的模板,模板中也可能包含帶有模板的指令。

模板樹可能又大又深,盡管元素可以被多個指令所支持或修飾,這些指令本身的模板中也可以包含其他指令,但只有屬於最高優先級指令的模板會被解析並添加到模板樹中。
將包含模板的指令和添加行為的指令分離開來。

一旦對指令和其中的子模板進行遍歷或編譯,編譯后的模板會返回一個叫做模板函數的函數。我們有機會在指令的模板函數被返回前,對編譯后的DOM樹進行修改。
以ng-repeat為例,它會遍歷指定的數組或對象,在數據綁定之前構建出對應的DOM結構。   只會有很少的性能開銷。

一個指令的表現一旦編譯完成,馬上就可以通過編譯函數對其進行訪問,編譯函數的簽名包含有訪問指令聲明所在的元素(tElemente)及該元素其他屬性(tAttrs)的方法。這個編譯函數返回前面提到的模板函數,其中含有完整的解析樹。

 

編譯函數:conpile()

compile函數 可以返回一個對象或函數。理解compile和link選項是AngularJS中需要深入討論的高級話題之一,對於了解AngularJS究竟是如何工作的至關重要。
compile選項本身並不會被頻繁使用,但是link函數則會被經常使用。本質上,當我們設置了link選項,實際上是創建了一個postLink()鏈接函數,以便compile()函數可以定義鏈接函數。
如果設置了compile函數,說明我們希望在指令和實時數據被放到DOM中之前進行DOM操作在這個函數中進行諸如添加和刪除節點等DOM操作是安全的

compile和link選項是互斥的。如果同時設置了這兩個選項,那么會把compile所返回的函數當作鏈接函數,而link選項本身則會被忽略。

compile: function(tEle, tAttrs, transcludeFn) {
    var tplEl = angular.element('<div>' +'<h2></h2>' +'</div>');
    var h2 = tplEl.find('h2');
    h2.attr('type', tAttrs.type);
    h2.attr('ng-model', tAttrs.ngModel);
    h2.val("hello");
    tEle.replaceWith(tplEl);
    
    return function(scope, ele, attrs) {
        // 連接函數
    };
}

不要進行DOM事件監聽器的注冊:這個操作應該在鏈接函數(link)中完成。

編譯函數負責對模板DOM進行轉換。    鏈接函數負責將作用域和DOM進行鏈接。 

 

link函數

用link函數創建可以操作DOM的指令。

鏈接函數是可選的。如果定義了編譯函數,它會返回鏈接函數,因此當兩個函數都定義了時,編譯函數會重載鏈接函數。

如果我們的指令很簡單,並且不需要額外的設置,可以從工廠函數 (回調函數)返回一個函數來代替對象。如果這樣做了,這個函數就是鏈接函數

下面兩種定義指令的方式在功能上是完全一樣的:

angular.module('myApp', [])
       .directive('myDirective', function() {
            return {
                    pre: function(tElement, tAttrs, transclude) {
                        // 在子元素被鏈接之前執行
                        // 在這里進行Don轉換不安全
                        // 之后調用'lihk'h函數將無法定位要鏈接的元素
                    },
                    post: function(scope, iElement, iAttrs, controller) {
                        // 在子元素被鏈接之后執行
                        // 如果在這里省略掉編譯選項
                        //在這里執行DOM轉換和鏈接函數一樣安全嗎
                    }
            };
        });

angular.module('myApp', [])
       .directive('myDirective', function() {
            return {
                link: function(scope, ele, attrs) {
                    return {
                            pre: function(tElement, tAttrs, transclude) {
                                // 在子元素被鏈接之前執行
                                // 在這里進行Don轉換不安全
                                // 之后調用'lihk'h函數將無法定位要鏈接的元素
                            },
                            post: function(scope, iElement, iAttrs, controller) {
                                // 在子元素被鏈接之后執行
                                // 如果在這里省略掉編譯選項
                                //在這里執行DOM轉換和鏈接函數一樣安全嗎
                            }
                        
                    }
                }
            }
});

當定義了編譯函數來取代鏈接函數時,鏈接函數是我們能提供給返回的 就是 postLink函數。

鏈接函數的作用。它會在模板編譯並同作用域進行鏈接后被調用,因此它負責設置事件監聽器,監視數據變化和實時的操作DOM。

link函數對綁定了實時數據的DOM具有控制能力,因此需要考慮性能問題。在選擇是用編譯函數還是鏈接函數實現功能時,將性能影響考慮進去。

鏈接函數的簽名如下:
link: function(scope, element, attrs) {
  // 在這里操作DOM
}

如果指令定義中有require選項,函數簽名中會有第四個參數,代表着所依賴的控制器 或者 指令的控制器
  // require 'SomeController',
  link: function(scope, element, attrs, SomeController) {
      // 在這里操作DOM,可以訪問required指定的控制器
  }

ngMode

ngModel是一個用法特殊的指令,它提供更底層的API來處理控制器內的數據。

ngModel控制器會隨ngModel被一直注入到指令中,其中包含了一些方法。為了訪問ngModelController必須使用require設置(像前面的例子中那樣):

angular.module('myApp')
.directive('myDirective',function(){
    return {
        require: '?ngModel',
        link: function(scope, ele, attrs, ngModel) {
            if (!ngModel) return;
        // 現在我們的指令中已經有ngModelController的一個實例
        }
    };
});

如果不設置require選項, ngModelController就不會被注入到指令中。

這個指令沒有隔離作用域。如果給這個指令設置隔離作用域,將導致內部ngModel無法更新外部ngModel的對應值: AngularJS會在本地作用域以外查詢值。

設置作用域中的視圖值,調用ngModel.$setViewValue()函數。 ngModel.$setViewValue()函數可以接受一個參數,參數是我們想要賦值給ngModel實例的實際值。這個方法會更新控制器上本地的$viewValue,然后將值傳遞給每一個$parser函數(包括驗證器)。

當值被解析,且$parser流水線中所有的函數都調用完成后,值會被賦給$modelValue屬性,並且傳遞給指令中ng-model屬性提供的表達式。
所有步驟都完成后, $viewChangeListeners中所有的監聽器都會被調用。注意,單獨調用$setViewValue()不會喚起一個新的digest循環,因此如果想更新指令,需要在設置$viewValue后手動觸發digest。

$setViewValue()方法適合於在自定義指令中監聽自定義事件(比如使用具有回調函數的jQuery插件),我們會希望在回調時設置$viewValue並執行digest循環。

angular.module('myApp')
.directive('myDirective', function() {
    return {
        require: '?ngModel',
        link: function(scope, ele, attrs, ngModel) {
            if (!ngModel) return;
            $(function() {
                ele.datepicker({
                    onSelect: function(date) {
                        // 設置視圖和調用apply
                        scope.$apply(function() {
                            ngModel.$setViewValue(date);
                        });
                    }
                });
            });
        }
    };
});

 

ngModelController中有幾個屬性可以用來檢查甚至修改視圖:

1. $viewValue
$viewValue屬性保存着更新視圖所需的實際字符串。
2. $modelValue
$modelValue由數據模型持有。 $modelValue和$viewValue可能是不同的,取決於$parser流水線是否對其進行了操作

3. $parsers

$parsers的值是一個由函數組成的數組,其中的函數會以流水線的形式被逐一調用。ngModel從DOM中讀取的值會被傳入$parsers中的函數,並依次被其中的解析器處理
4. $formatters
$formatters的值是一個由函數組成的數組,其中的函數會以流水線的形式在數據模型的值發生變化時被逐一調用。它和$parser流水線互不影響,用來對值進行格式化和轉換,以便在綁定了這個值的控件中顯示。
5. $viewChangeListeners
$viewChangeListeners的值是一個由函數組成的數組,其中的函數會以流水線的形式在視圖中的值發生變化時被逐一調用。通過$viewChangeListeners,可以在無需使用$watch的情況下實現類似的行為。由於返回值會被忽略,因此這些函數不需要返回值。
6. $error
$error對象中保存着沒有通過驗證的驗證器名稱以及對應的錯誤信息。
7. $pristine
$pristine的值是布爾型的,可以告訴我們用戶是否對控件進行了修改。
8. $dirty
$dirty的值和$pristine相反,可以告訴我們用戶是否和控件進行過交互。
9. $valid
$valid值可以告訴我們當前的控件中是否有錯誤。當有錯誤時值為false, 沒有錯誤時值為true。
10. $invalid
$invalid值可以告訴我們當前控件中是否存在至少一個錯誤,它的值和$valid相反。

自定義驗證:

angular.module('validationExample', [])
.directive('ensureUnique',function($http) {
    return {
        require: 'ngModel',
        link: function(scope, ele, attrs, c) {
            scope.$watch(attrs.ngModel, function() {
                $http({
                    method: 'POST',
                    url: '/api/check/' + attrs.ensureUnique,
                    data: {field: attrs.ensureUnique, valud:scope.ngModel
                    }).success(function(data,status,headers,cfg) {
                        c.$setValidity('unique', data.isUnique);
                    }).error(function(data,status,headers,cfg) {
                        c.$setValidity('unique', false);
                    });
            });
        }
    };
});  

 

出於演示目的,盡管我們在指令內置入了一個$http調用,但是在產品中的指令內使用$http是不明智的。相反,將它置入到服務中會更好。關於服務的更多信息請參第14章。


<input type="text"  placeholder="Desired username"  name="username“  ng-model="signup.username"
            ng-minlength="3"  ng-maxlength="20"  ensure-unique="username" required />

每當ngModel中對應的字段發生變化就會向服務器發送請求,以檢查用戶名是否是唯一的

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM