1. 程式人生 > >angularjs雙向繫結後,發生了什麼事情?是什麼可以讓view層和controller層進行繫結的?

angularjs雙向繫結後,發生了什麼事情?是什麼可以讓view層和controller層進行繫結的?

這裡是修真院前端小課堂,每篇分享文從

【背景介紹】【知識剖析】【常見問題】【解決方案】【編碼實戰】【擴充套件思考】【更多討論】【參考文獻】

八個方面深度解析前端知識/技能,本篇分享的是:

【angularjs雙向繫結後,發生了什麼事情?是什麼可以讓view層和controller層進行繫結的?    】

 

1.1資料繫結
我們看到的網站頁面中,是由資料和設計兩部分組合而成。將設計轉換成瀏覽器能理解的語言,便是html和css主要做的工作。 而將資料顯示在頁面上,並且有一定的互動效果(比如點選等使用者操作及對應的頁面反應)則是js主要完成的工作。 很多時候我們不可能每次更新資料便重新整理頁面(get請求),而是通過向後端請求相關資料,並通過無重新整理載入的方式進行更新頁面(post請求)。 那麼資料進行更新後,頁面上相應的位置也能自動做出對應的修改,便是資料繫結。

在以前的開發模式中,這一步一般通過jq操作DOM結構,從而進行更新頁面。但這樣帶來的是大量的程式碼和大量的操作。 如果能在開始的時候,便已經確定好從後端獲取的資料到頁面上需要進行的操作,當資料發生改變,頁面的相關內容也自動發生變化,這樣便能極大地方便前端工程師的開發。 在新的框架中(angualr,react,vue等),通過對資料的監視,發現變化便根據已經寫好的規則進行修改頁面,便實現了資料繫結。 可以看出,資料繫結是M(model,資料)通過VM(model-view,資料與頁面之間的變換規則)向V(view)的一個修改。

而雙向繫結則是增加了一條反向的路。在使用者操作頁面(比如在Input中輸入值)的時候,資料能及時發生變化,並且根據資料的變化, 頁面的另一處也做出對應的修改。有一個常見的例子就是淘寶中的購物車,在商品數量發生變化的時候,商品價格也能及時變化。這樣便實現了V——M——VM——V的一個雙向繫結。

 

 

ANGULARJS雙向繫結後,發生了什麼事情? 什麼讓VIEW層和CONTROLLER層進行繫結的Angular雙向繫結通過watch,digest和apply實現的

watch序列 watch監控model中是否有變化,會記錄last值,也就是改變後的值,每一個model都會增加一個watch到watch佇列中。 傳遞給$watch()的第二個引數稱為監聽器函式,當aModel的值發生變化時,它就被呼叫。我們很容易理解,當aModel的值發生改變, 這個監聽器就會被呼叫來更新HTML中的表示式。但是,還有一個很重要的問題!Angular是怎麼判斷什麼時候呼叫這個監聽器函式的呢? 換句話說,AngularJS是如何知道aModel值是何時發生改變的,從而它可以呼叫相應的監聽器函式呢?它是否定期執行一個函式來檢查 scope 模型的值是否已經改變了?好,這就是 $digest 迴圈的步驟。

在 $digest 週期中,watcher 會被觸發。當一個 watcher 被觸發時,AngularJS將評估 scope 模型,如果它發生了變化,則呼叫相應的監聽器函式。 那麼,我們的下一個問題是,這個$digest 迴圈是何時開始的。 $digest迴圈是在什麼時候以各種方式開始的? 當瀏覽器接收到可以被 angular context 處理的事件時,digest迴圈就會觸發,,遍歷所有的watch,最後更新 dom。 假設你通過ng-click指令在處理程式函式中更改了一個scope模型。在這種情況下,AngularJS會通過呼叫 $digest() 自動觸發一個 $digest 迴圈。當 $digest 迴圈開始的時候,它就會觸發每一個 watcher。 這些 watcher 會檢查scope模型的當前值是否與上次計算得到的值不同。如果不同,則執行相應的監聽器函式。 因此,如果在檢視中有任何表示式,它們將被更新。除了ng-click之外,還有其他一些內建的指令/服務可以讓你更改模型(例如ng-model、$timeout 等),並自動觸發一個 $digest 迴圈。

例子

click 時會產生一次更新的操作(至少觸發兩次 $digest 迴圈)  •按下按鈕 •瀏覽器接收到一個事件,進入到 angular context •digest迴圈開始執行,查詢每個watch 是否變化  •由於監視 scope.val的 scope.val的watch 報告了變化,因此強制再執行一次 $digest 迴圈  •新的 $digest 迴圈未檢測到變化 •瀏覽器拿回控制器,更新 $scope. val.新值對應的 dom 到目前為止還不錯!但是,這裡有一個小問題。在上面的例子中,Angular並不直接呼叫 $digest()。 相反,它呼叫 $scope.$apply(),而 $scope.$apply() 又會呼叫 $rootScope.$digest()。

 

因此,一個 $digest 迴圈開始於 $rootScope,隨後會訪問所有的child scopes,並在此過程中呼叫child scopes中的watchers。 現在,假設你將一個ng-click指令附加到一個按鈕,並將一個函式名傳遞給它。當單擊按鈕時,AngularJS將函式呼叫包裝在 $scope.$apply() 中。因此,你的函式照常執行,更改模型(如果有的話),並開始一個 $digest 迴圈來確保你的更改反映在檢視中。 注意:$scope.$apply() 自動呼叫 $rootScope.$digest()。$apply() 函式有兩種形式。第一種接受一個函式作為引數,執行這個函式,並觸發一個 $digest 迴圈。第二種則不需要任何引數,在呼叫時只觸發一個 $digest 迴圈。

$APPLY

進行資料變化檢查的實際上是$digest函式,但是我們往往不是直接使用$digest,而是使用$apply, $apply接收表示式或者函式作為引數後呼叫$digest來更新繫結部門以及監控器。實際上,Angular幾乎在所有提供的程式碼中添加了$apply, 如ng-click,初始controller,$http的回撥操作,在這,你並不需要親自呼叫 $apply,而且重複的呼叫會引起錯誤。因此,當你運行了一個新階段,並且這部分並不屬於Angular庫的情況下才需要使用$apply。 這有一段關於setTimeout的程式碼,在經過了2000毫秒的延遲之後,程式碼進入執行了一個新的階段,但是Angular並不知道資料有更新, 因此更新並不會被顯示。我們應該用angular JS提供的timeout方法,這樣它就會被自動用 timeout方法,這樣它就會被自動用apply方法包起來了

 

什麼時候用$APPLY()

那我們到底什麼時候需要去呼叫apply()方法呢?情況非常少,實際上幾乎我們所有的程式碼都包在scope.apply()裡面, 像ng−click,controller的初始化,http的回撥函式等。在這些情況下,我們不需要自己呼叫,實際上我們也不能自己呼叫,否則在apply()方法裡面再呼叫 apply()方法會丟擲錯誤。如果我們需要在一個新的執行序列中執行程式碼時才真正需要用到它, 而且當且僅當這個新的執行序列不是被angular JS的庫的方法建立的,這個時候我們需要將程式碼用 scope.apply()包起來。

 

$DIGEST 迴圈會執行多少次?

$digest 迴圈的上限是 10 次(超過 10次後丟擲一個異常,防止無限迴圈)。 $digest 迴圈不會只執行一次。在當前的一次迴圈結束後, 它會再執行一次迴圈用來檢查是否有 models 發生了變化。 這就是髒檢查(Dirty Checking),它用來處理在 listener 函式被執行時可能引起的 model 變化。 因此 digest迴圈會持續執行直到model不再發生變化,或者 digest迴圈會持續執行直到model不再發生變化,或者digest 迴圈的次數達到了 10 次(超過 10 次後丟擲一個異常,防止無限迴圈)。 當 $digest 迴圈結束時,DOM 相應地變化。

 

髒檢查如何被觸發

angular 會在可能觸發 UI 變更的時候進行髒檢查:這句話並不準確。實際上髒檢查是digest執行的, 另一個更常用的用於觸發髒檢查的函式apply——其實就是 $digest 的一個簡單封裝(還做了一些抓異常的工作)。 通常寫程式碼時我們無需主動呼叫 apply或 apply或digest 是因為 angular 在外部對我們的回撥函式做了包裝。 例如常用的 ng-click,這是一個指令(Directive),$digest過程的邏輯就是檢查watcher列表中的每一項,看當前值與上次的值是否相同, 如果不同則呼叫listener回撥函式。這就是dirty-checking的核心邏輯

 

在 AngularJS 中使用 $watch注意事項?

 

 

 

 

我們在實際運用中常常不只是對一個原始型別的屬性進行監視,如果你還記得Javascript中的六種基本型別, 你一定會記得原始型別(數字,字串)和引用型別的區別。對於原始型別,如果我們使用了一個賦值操作, 則這個原始型別變數會“真正的”被進行一次複製,然而對於引用型別,在進行賦值時,僅僅時將賦值的變數指向了這個引用型別。 在AngularJS的$watch方法中,對兩者的操作也有不同之處。原始型別,就像我們上面例子中提到的$rootScope, 沒有什麼特別之處,然而如果要對一個引用型別,尤其是在實際運用中常見的物件陣列進行監視時,情況就不一樣了。

$watch在對待原始型別和引用型別會有不同的處理方式,這就要首先說一說$watch函式的第三個引數。 在前面的例子中,我們知道,$watch函式有接收兩個引數,第一個引數是需要監視的物件,第二個引數是在監視物件發生變化時需要呼叫的函式, 實際上$watch還有第三個引數,它在預設情況下是false。在預設情況下,即不顯式指明第三個引數或者將其指明為false時, 我們進行的監視叫做“引用監視”。引用監視的原詞的“reference watch”,它的意思是隻要監視的物件引用沒有發生變化, 就不算它發生了變化。具體來說,在上面的例子中,只要是items的引用沒有發生變化,就算items中的一些屬性發生了變化, $watch也會當做沒有看見。那麼在什麼時候算是引用發生了變化呢?比如說將一個新的陣列newItems賦值給items,此時$watch才會站出來

相反,如果我們將$watch的第三個變數設定為true,那麼此時我們進行的監視叫做“全等監視”, 原詞是“equality watch”。此時,$watch就像是一個醋意十足的戀人,只要看他的物件有一點風吹草動,馬上就跳出來, 既然全等監視這麼好,那麼我們為什麼不直接用全等監視呢?當然,任何事情都有好的壞的兩個方面,全等監視固然是好, 但是它在執行時需要先遍歷整個監視物件,然後在每次$digest之前使用angular.copy()將整個物件深拷貝一遍 然後在執行之後用angular.equal()將前後的物件進行對比,上面的例子中因為items比較簡單,因此可能效能上不會有什麼差別, 但是到了實際生產時,我們要面對的資料千千萬萬,可能因為全等監視這一個設定就會消耗大量的資源,讓應用停滯不前。 因此這就需要我們在使用時進行權衡,究竟應該使用哪一種監視方式。

 

 

髒檢查慢嗎?

說實話髒檢查效率是不高,但是也談不上有多慢。簡單的數字或字串比較能有多慢呢?十幾個表示式的髒檢查可以直接忽略不計; 上百個也可以接受;成百上千個就有很大問題了。繫結大量表達式時請注意所繫結的表示式效率。建議注意一下幾點: •表示式(以及表示式所呼叫的函式)中少寫太過複雜的邏輯 •不要連線太長的 filter(往往 filter 裡都會遍歷並且生成新陣列) •不要訪問 DOM 元素。 1、使用單次繫結減少繫結表示式數量 單次繫結(One-time binding 是 Angular 1.3 就引入的一種特殊的表示式, 它以 :: 開頭,當髒檢查發現這種表示式的值不為 undefined 時就認為此表示式已經穩定,並取消對此表示式的監視。 這是一種行之有效的減少繫結表示式數量的方法,與ng-repeat 連用效果更佳,但過度使用也容易引發 bug。 2、善用 ng-if 減少繫結表示式的數量

 

問題一:為什麼例五里面自定義指令需要呼叫$apply方法?是不是自定義指令都不具備$apply方法?

答:不是。不是因為自定義指令本身不具有$apply方法,因為element.on(“click”,function{})這實際上是一個jQuery方法,而jQuery是不具備$apply方法的。如果我們把scope.b++使用一個angular方法去觸發,是不用呼叫$apply就可以觸發的。

問題二:前文提到的迴圈十次會丟擲異常,是指什麼?

是指在一個ng-指令(本身已經自帶$apply方法)內部再次呼叫$apply方法,就會丟擲異常。這實際上是angular的保護機制。

問題三:dirty-checking這麼複雜,會不會速度很慢?

不會的,實際上執行的很快。而且在ES6普及後,angular的未來版本會加入Object.observe,$digest迴圈的速度會更快。

 

 

PPT連結 視訊連結

更多內容,可以加入IT交流群565734203與大家一起討論交流

這裡是技能樹·IT修真院:https://www.jnshu.com,初學者轉行到網際網路的聚集地