1、ng-if ng-show對比
ng-show、ng-hide、ng-if指令都可以用來控制dom元素的顯示或隱藏。
1)實現上:ng-show和ng-hide根據所給表達式的值來顯示或隱藏HTML元素。當賦值給ng-show指令的值為false時元素會被隱藏,值為true時元素會顯示。ng-hide功能類似,使用方式相反。元素的顯示或隱藏是通過改變CSS的display屬性值來實現的。
ng-if指令可以根據表達式的值在DOM中生成或移除一個元素。如果賦值給ng-if的表達式的值是false,那對應的元素將會從DOM中移除,否則生成一個新的元素插入DOM中。ng-if同no-show和ng-hide指令最本質的區別是,它不是通過CSS顯示或隱藏DOM節點,而是刪除或者新增結點。
然后你將所有的ng-show換成ng-if,你會發現性能瞬間快的像兩個應用.原因在ng-show還是會執行其中的所有綁定,ng-if則會在等於true,也就是顯示的時候再去執行其中的綁定.這樣一來性能就有很大的提高。
<div ng-if='true'> <input type='text' ng-model='a'> </div>
<span>{{a}}</span>
在輸入框輸入a也不會改變span中顯示的值。如果a沒有在父scope中初始化,那么scope就沒有辦法繼承,這樣子導致的問題就是父scope修改了a后並沒有得到相應的效果。如果在ng-if之前先把參數都初始化就可以避免失去了控制權。
對於ng-if中ng-model的解決辦法就是使用對象,或者使用$parent
<div ng-if='true'> <input type='text' ng-model='a.id'> </div>
<span>{{a}}</span>
.controller('myCtrl',function($scope){ $scope.a={}; })
span中會同步顯示{'id':輸入的值}
或者使用$parent
<div ng-if='true'> <input type='text' ng-model='$parent.a'> </div> <span>{{a}}</span>
這個不需要初始化
6)ng-if中作用域分析
作用域:每個 Angular 應用默認有一個根作用域 $rootScope, 根作用域位於最頂層,從它往下掛着各級作用域。通常情況下,頁面中 ng-model
綁定的變量都是在對應的 Controller 中定義的。如果一個變量未在當前作用域中定義,JavaScript 會通過當前 Controller 的 prototype 向上查找,也就是作用域的繼承。
這又分兩種情況。
基本類型變量:
<div ng-controller="OuterCtrl">
<p>{{x}}</p>
<div ng-controller="InnerCtrl">
<input type="text" ng-model="x">
</div>
</div>
運行后會發現跟文章開頭一樣的問題,里面輸入框變了,外面的沒跟着變。原因在於,InnerCtrl
中並未定義 x
這個變量,取值的時候,會沿着原型鏈向上找,找到了 OuterCtrl
中定義的 x
,然后賦值給自己,在 InnerCtrl
的輸入框輸入值時,改變的是 InnerCtrl
中的 x
,而對 OuterCtrl
中的 x
無影響。此時,兩個 x
是獨立的。
不過,如果你不嫌麻煩的話,用 $scope.$parent
可以綁定並影響上一層作用域中的基本變量:
1 |
<input type="text" ng-model="$parent.x"> |
引用類型的變量:在這種情況下,兩者的 data
是同一個引用,對這個對象上面的屬性修改,是可以反映到兩級對象上的。Angular的實現機制其實也就是把這兩個控制器中的$scope作了關聯,外層的作用域實例成為了內層作用域的原型。也就是inner(內層作用域實例)-->inner.prototype-->outer(外層作用域實例),因此inner,outer引用了同一個對象,對這個對象的修改會影響到inner,outer
並不是只有 Controller 可以創建作用域,ng-if
等指令也會(隱式地)產生新作用域。
總結下來就是,ng-if
、 ng-switch
、 ng-include
等會動態創建一塊界面的東西,都是自帶一級作用域。
因此,在開發過程中,為了避免模板中的變量歧義,應當盡可能使用命名限定,比如 data.x
,出現歧義的可能性就比單獨的 x
要少得多。
2、ng-repeat迭代數組的時候,如果數組中有相同值,會有什么問題,如何解決?
會提示 Duplicates in a repeater are not allowed.
加 track by $index
可解決。當然,也可以 trace by 任何一個普通的值,只要能唯一性標識數組中的每一項即可(建立 dom 和數據之間的關聯)。
ng-repeat當數組中有相同的項時會出現這個問題。
<p ng-repeat='item in array'>{{item}}</p> <script> $scope.array=['array','2','array']; </script>
因為array中有兩個array所以會報錯,加上item in array track by $index可以解決
<p ng-repeat='item in array'>{{item.id}}</p> <script> $scope.array=[{id:1,similar:'array'},{id:1,similar:'array'},{id:2}]; </script>
這里不會報錯,item指向的是對象,這兩個對象不同
3、ng-click 中寫的表達式,能使用 JS 原生對象上的方法嗎?
不止是 ng-click 中的表達式,只要是在頁面中,都不能直接調用原生的 JS 方法,因為這些並不存在於與頁面對應的 Controller 的 $scope 中。
舉個栗子:
<p>{{parseInt(55.66)}}<p>
會發現,什么也沒有顯示。
但如果在 $scope 中添加了這個函數:
$scope.parseInt = function(x){ return parseInt(x); }
這樣自然是沒什么問題了。
對於這種需求,使用一個 filter 或許是不錯的選擇:
<p>{{13.14 | parseIntFilter}}</p> app.filter('parseIntFilter', function(){ return function(item){ return parseInt(item); } })
4、{{now | 'yyyy-MM-dd'}}
這種表達式里面,豎線和后面的參數通過什么方式可以自定義?
filter,格式化數據,接收一個輸入,按某規則處理,返回處理結果。
內置 filter
ng 內置的 filter 有九種:
-
date(日期)
-
currency(貨幣)
-
limitTo(限制數組或字符串長度)
-
orderBy(排序)
-
lowercase(小寫)
-
uppercase(大寫)
-
number(格式化數字,加上千位分隔符,並接收參數限定小數點位數)
-
filter(處理一個數組,過濾出含有某個子串的元素)
-
json(格式化 json 對象)
filter 有兩種使用方法,一種是直接在頁面里:
<p>{{now | date : 'yyyy-MM-dd'}}</p>
另一種是在 js 里面用:
// $filter('過濾器名稱')(需要過濾的對象, 參數1, 參數2,...) $filter('date')(now, 'yyyy-MM-dd hh:mm:ss');
自定義 filter
// 形式 app.filter('過濾器名稱',function(){ return function(需要過濾的對象,過濾器參數1,過濾器參數2,...){ //...做一些事情 return 處理后的對象; } }); // 栗子 app.filter('timesFilter', function(){ return function(item, times){ var result = ''; for(var i = 0; i < times; i++){ result += item; } return result; } })
5、factory、service 和 provider 是什么關系?
查看http://www.oschina.net/translate/angularjs-factory-vs-service-vs-provider
不要往 controller 和 scope 里堆滿不必要的邏輯。controller 這一層應該很薄;也就是說,應用里大部分的業務邏輯和持久化數據都應該放在 service 里。關於如何在 controller 里保存持久化數據。這就不是 controller 該干的事。出於內存性能的考慮,controller 只在需要的時候才會初始化,一旦不需要就會被拋棄。因此,每次當你切換或刷新頁面的時候,Angular 會清空當前的 controller。與此同時,service 可以用來永久保存應用的數據,並且這些數據可以在不同的 controller 之間使用。
Angular 提供了3種方法來創建並注冊我們自己的 service。
-
Factory
-
Service
-
Provider
1) 用 Factory 就是創建一個對象,為它添加屬性,然后把這個對象返回出來。你把 service 傳進 controller 之后,在 controller 里這個對象里的屬性就可以通過 factory 使用了。
2) Service 是用"new"關鍵字實例化的。因此,你應該給"this"添加屬性,然后 service 返回"this"。你把 service 傳進 controller 之后,在controller里 "this" 上的屬性就可以通過 service 來使用了。
3) Providers 是唯一一種你可以傳進 .config() 函數的 service。當你想要在 service 對象啟用之前,先進行模塊范圍的配置,那就應該用 provider。
可以把Provider想象成由兩部分組成。第一部分的變量和函數是可以在app.config函數中訪問的,因此你可以在它們被其他地方訪問到之前來修改它們。第二部分 的變量和函數是可以在任何傳入了’myProvider‘的控制器中進行訪問的。當你使用Provider創建一個service時,唯一的可以在你的控制器中訪問的屬性和方法是通過$get()函數返回內容。
從底層實現上來看,service 調用了 factory,返回其實例;factory 調用了 provider,返回其 $get
中定義的內容。factory 和 service 功能類似,只不過 factory 是普通 function,可以返回任何東西(return 的都可以被訪問);service 是構造器,可以不返回(綁定到 this 的都可以被訪問);provider 是加強版 factory,返回一個可配置的 factory。
6、angular 的數據綁定采用什么機制?詳述原理
臟檢查機制。
我們的瀏覽器一直在等待事件,比如用戶交互。假如你點擊一個按鈕或者在輸入框里輸入東西,事件的回調函數就會在javascript解釋器里執行,然后你就可以做任何DOM操作,等回調函數執行完畢時,瀏覽器就會相應地對DOM做出變化。 Angular拓展了這個事件循環,生成一個有時稱為angular context
的執行環境。
1)$watch隊列,每次你綁定一些東西到你的UI上時你就會往$watch隊列里插入一條$watch
。
<ul> <li ng-repeat="person in people"> {{person.name}} - {{person.age}} </li> </ul>
這里又生成了多少個$watch
呢?每個person有兩個(一個name,一個age),然后ng-repeat又有一個,因此10個person一共是(2 * 10) +1
,也就是說有21個$watch
。 因此,每一個綁定到了UI上的數據都會生成一個$watch
。對,那這寫$watch
是什么時候生成的呢? 當我們的模版加載完畢時,也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular解釋器會尋找每個directive,然后生成每個需要的$watch
。
2)$digest
當瀏覽器接收到可以被angular context
處理的事件時,$digest
循環就會觸發。這個循環是由兩個更小的循環組合起來的。一個處理evalAsync
隊列,另一個處理$watch
隊列。 這個是處理什么的呢?$digest
將會遍歷我們的$watch
,然后詢問:
- 嘿,
$watch
,你的值是什么?- 是9。
- 好的,它改變過嗎?
- 沒有,先生。
- (這個變量沒變過,那下一個)
- 你呢,你的值是多少?
- 報告,是
Foo
。
- 報告,是
- 剛才改變過沒?
- 改變過,剛才是
Bar
。
- 改變過,剛才是
- (很好,我們有DOM需要更新了)
- 繼續詢問知道
$watch
隊列都檢查過。
這就是所謂的dirty-checking
。既然所有的$watch
都檢查完了,那就要問了:有沒有$watch
更新過?如果有至少一個更新過,這個循環就會再次觸發,直到所有的$watch
都沒有變化。這樣就能夠保證每個model都已經不會再變化。記住如果循環超過10次的話,它將會拋出一個異常,防止無限循環。 當$digest
循環結束時,DOM相應地變化。
比如
{{ name }} <button ng-click="changeFoo()">Change the name</button>
這里我們有一個$watch
因為ng-click不生成$watch
(函數是不會變的)。
- 我們按下按鈕
- 瀏覽器接收到一個事件,進入
angular context
。 $digest
循環開始執行,查詢每個$watch
是否變化。- 由於監視
$scope.name
的$watch
報告了變化,它會強制再執行一次$digest
循環。 - 新的
$digest
循環沒有檢測到變化。 - 瀏覽器拿回控制權,更新與
$scope.name
新值相應部分的DOM。
3)$apply
誰決定什么事件進入angular context
,而哪些又不進入呢?$apply
!
如果當事件觸發時,你調用$apply
,它會進入angular context
,如果沒有調用就不會進入。現在你可能會問:剛才的例子里我也沒有調用$apply
啊,為什么?Angular為了做了!因此你點擊帶有ng-click的元素時,時間就會被封裝到一個$apply
調用。如果你有一個ng-model="foo"
的輸入框,然后你敲一個f
,事件就會這樣調用$apply("foo = 'f';")
。
那么上面的過程就可以更新為:
按下按鈕,瀏覽器接受到一個事件,這個事件被包裹在apply中,就會進入angular context
4)什么時候需要手動調用$apply
比如有一個directive和一個controller
在controller里面初始化一個數字為0
在directive里面創建了一個可點擊區域,點擊該區域這個數字就會自增1,然后頁面還有一個按鈕,有一個ng-click事件,點擊按鈕也會自增1
但是你在點擊這個可點擊區域時,控制台顯示數字自增了,可是頁面沒顯示。而按鈕點擊的時候控制台和頁面都會自增1.
這是因為ng-click會自己觸發apply,開始digest循環,檢查哪些watch變化了,並更新頁面,但是你自己的這個點擊區域就不會了,你沒有用apply,他只是變化了后台的數字,沒有觸發digest,不會檢查有沒有發生變化啊,所以你需要將這個可點擊區域的點擊事件包裹在一個apply里面,這樣就可以實現和按鈕一樣的功能了。
當然上面的例子也可以直接調用無參的apply,差別就是在第一個版本中,我們是在angular context
的外面更新的數據,如果有發生錯誤,Angular永遠不知道。很明顯在這個像個小玩具的例子里面不會出什么大錯,但是想象一下我們如果有個alert框顯示錯誤給用戶,然后我們有個第三方的庫進行一個網絡調用然后失敗了,如果我們不把它封裝進$apply
里面,Angular永遠不會知道失敗了,alert框就永遠不會彈出來了。
因此,如果你想使用一個jQuery插件,並且要執行$digest
循環來更新你的DOM的話,要確保你調用了$apply
。
5)$watch的使用
app.controller('MainCtrl', function($scope) { $scope.name = "Angular"; $scope.updated = -1; $scope.$watch('name', function() { $scope.updated++; }); });
index.html
<body ng-controller="MainCtrl"> <input ng-model="name" /> Name updated: {{updated}} times. </body>
這就是我們創造一個新的$watch
的方法。第一個參數是一個字符串或者函數,在這里是只是一個字符串,就是我們要監視的變量的名字,在這里,$scope.name
(注意我們只需要用name
)。第二個參數是當$watch
說我監視的表達式發生變化后要執行的。我們要知道的第一件事就是當controller執行到這個$watch
時,它會立即執行一次,因此我們設置updated為-1
如果想要監視對象
$scope.$watch('user', function(newValue, oldValue) { if (newValue === oldValue) { return; } $scope.updated++; });
index.html
<body ng-controller="MainCtrl"> <input ng-model="user.name" /> Name updated: {{updated}} times. </body>
呃?沒用,為啥?因為$watch
默認是比較兩個對象所引用的是否相同,在例子1和2里面,每次更改$scope.name
都會創建一個新的基本變量,因此$watch
會執行,因為對這個變量的引用已經改變了。在上面的例子里,我們在監視$scope.user
,當我們改變$scope.user.name
時,對$scope.user
的引用是不會改變的,我們只是每次創建了一個新的$scope.user.name
,但是$scope.user
永遠是一樣的。
$scope.$watch('user', function(newValue, oldValue) { if (newValue === oldValue) { return; } $scope.updated++; }, true);
現在有用了吧!因為我們對$watch
加入了第三個參數,它是一個bool類型的參數,表示的是我們比較的是對象的值而不是引用。由於當我們更新$scope.user.name
時$scope.user
也會改變,所以能夠正確觸發。
轉自http://www.angularjs.cn/A0a6 http://huangtengfei.com/2015/09/data-bind-of-angularjs/
7、如何理解angular中的作用域機制
Angular應用是分層的,主要有三個層面:視圖(view),模型(model),視圖模型(viewModel),即MVVM。其中,視圖很好理解,就是直接可見的界面,模型就是數據,那么視圖模型是一種把數據包裝給視圖調用的東西。所謂作用域,也就是視圖模型中的一個概念。
1) rootScope
作用域在一個Angular應用中是以樹的形狀體現的,根作用域位於最頂層,從它往下掛着各級作用域。每一級作用域上面掛着變量和方法,供所屬的視圖調用。
如果想要在代碼中顯式使用根作用域,可以注入$rootScope。
2) 作用域的繼承關系
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> </div> </div> <script> function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) { } </script>
我們可以看到界面顯示了兩個1,而我們只在OuterCtrl的作用域里定義了a變量,但界面給我們的結果是,兩個a都有值。這里內層的a值顯然來自外層.在Angular中,如果兩個控制器所對應的視圖存在上下級關系,它們的作用域就自動產生繼承關系。用javascript來分析就是:
function Outer() { this.a = 1; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner();
Angular的實現機制其實也就是把這兩個控制器中的$scope作了關聯,外層的作用域實例成為了內層作用域的原型。以此類推,整個Angular應用的作用域,都存在自頂向下的繼承關系,最頂層的是$rootScope,然后一級一級,沿着不同的控制器往下,形成了一棵作用域的樹。
看一個例子:
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> <button ng-click="a=a+1">a++</button> </div> </div> <script> function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) {} </script>
點了按鈕之后,兩個a不一致了,里面的變了,外面的沒變,這是為什么?原先兩層不是共用一個a嗎,怎么會出現兩個不同的值?看這句就能明白了,相當於我們之前那個例子里,這樣賦值了:
function Outer() { this.a = 1; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner(); inner.a = inner.a + 1;
它有兩個過程,取值的時候,因為inner自身上面沒有,所以沿着原型往上取到了1,然后自增了之后,賦值給自己,這個賦值的時候就不同了,有a就賦值,沒有a,創造一個a也要賦值。所以這么一來,inner上面就被賦值了一個新的a,outer里面的仍然保持原樣,這也就導致了剛才看到的結果。
3)對象在上下級作用域之間的共享
我們就是想上下級共享變量,不創建新的,該怎么辦
function Outer() { this.data = { a: 1 }; } function Inner() { } var outer = new Outer(); Inner.prototype = outer; var inner = new Inner(); console.log(outer.data.a); console.log(inner.data.a); // 注意,這個時候會怎樣? inner.data.a += 1; console.log(outer.data.a); console.log(inner.data.a);
現在inner-->Inner.prototype-->outer-->Outer.prototype
所以inner和outer兩者的data是同一個引用,對這個對象上面的屬性修改,是可以反映到兩級對象上的。所以在angular中可以這樣使用
<div ng-controller="OuterCtrl"> <span>{{data.a}}</span> <div ng-controller="InnerCtrl"> <span>{{data.a}}</span> <button ng-click="data.a=data.a+1">increase a</button> </div> </div> <script> function OuterCtrl($scope) { $scope.data = { a: 1 }; } function InnerCtrl($scope) {} </script>
從這個例子我們就發現了,如果想要避免變量歧義,顯式指定所要使用的變量會是比較好的方式,那么如果我們確實就是要在上下級分別存在相同的變量該怎么辦呢,比如說下級的點擊,想要給上級的a增加1,我們可以使用$parent來指定上級作用域。
<div ng-controller="OuterCtrl"> <span>{{a}}</span> <div ng-controller="InnerCtrl"> <span>{{a}}</span> <button ng-click="$parent.a=a+1">increase a</button> </div> </div> <script> function OuterCtrl($scope) { $scope.a = 1; } function InnerCtrl($scope) {} </script>
4)控制器實例別名
在最近版本的AngularJS中,已經可以不顯式注入$scope了,語法是這樣:
<div ng-controller="CtrlB as instanceB"> <div>{{instanceB.a}}</div> <button ng-click="instanceB.foo()">click me</button> </div>
function CtrlB() { this.a = 1; this.foo = function() { }; } app.controller('CtrlB',CtrlB);
as語法,給CtrlB的實例取了一個別名叫做instanceB,這樣,它下屬的各級視圖都可以顯式使用這個名稱來調用其屬性和方法,不易引起歧義。
在開發過程中,為了避免模板中的變量歧義,應當盡可能使用命名限定,比如a.b,出現歧義的可能性就比單獨的b要少得多。
5)手動創建作用域
var newScope = scope.$new();
6) 作用域上的事件
我們剛才提到使用$parent來處理上下級的通訊,但其實這不是一種好的方式,尤其是在不同控制器之間,這會增加它們的耦合,對組件復用很不利。那怎樣才能更好地解耦呢?我們可以使用事件。
比如A1要傳播一個事件給B1
- 沿着父作用域一路往上到達雙方共同的祖先作用域,
$scope.$emit("someEvent", {});
- 從祖先作用域一級一級往下進行廣播,直到到達需要的地方
$scope.$broadcast("someEvent", {});
使用事件的主要作用是消除模塊間的耦合,發送方是不需要知道接收方的狀況的,接收方也不需要知道發送方的狀況,雙方只需要傳送必要的業務數據即可。
7)事件總線
我們能不能這樣:搞一個專門負責通訊的機構,大家的消息都發給它,然后由它發給相關人員,其他人員在理念上都是平級關系。
這就是一個很典型的訂閱發布模式,接收方在這里訂閱消息,發布方在這里發布消息。這個過程可以用這樣的圖形來表示:
代碼寫起來也很簡單,把它做成一個公共模塊,就可以被各種業務方調用了:
app.factory("EventBus", function() { var eventMap = {}; var EventBus = { on : function(eventType, handler) { //multiple event listener if (!eventMap[eventType]) { eventMap[eventType] = []; // 如果沒有該類型的事件,創造這個屬性,並初始化為一個空數組 } eventMap[eventType].push(handler); }, off : function(eventType, handler) { for (var i = 0; i < eventMap[eventType].length; i++) { if (eventMap[eventType][i] === handler) { eventMap[eventType].splice(i, 1); // 解除事件,將該類型事件中的處理函數刪除 break; } } }, fire : function(event) { var eventType = event.type; if (eventMap && eventMap[eventType]) { for (var i = 0; i < eventMap[eventType].length; i++) { eventMap[eventType][i](event); // 將eventMap對象中該類型事件的數組中的每一個事件都觸發,並傳入參數 } } } }; return EventBus; });
事件訂閱
EventBus.on("someEvent", function(event) { // 這里處理事件 var c = event.data.a + event.data.b; });
事件發布
EventBus.fire({ type: "someEvent", data: { aaa: 1, bbb: 2 } });
舉個例子,現在有個A頁面要向B,C頁面傳遞事件
在A頁面中:
EventBus.fire(dataA); dataA={ type: "click", data: { a: 1, b: 2 } }
在B頁面中
EventBus.on("click",handlerB); function handlerB(event) { // 這里處理事件 var c = event.data.a + event.data.b; }
在C頁面中
EventBus.on("click", handlerC); function handlerC(event) { // 這里處理事件 alert(event.data.a); }
因為factory是個單例對象,那么現在eventMap應該是這樣的
var eventMap = { click:[handlerB,handlerC] };
那么A頁面調用的fire實際上就是:handlerB(dataA),handlerC(dataA)
轉自https://github.com/xufei/blog/issues/18