angular中的$scope是頁面(view)和資料(model)之間的橋梁,它連結了頁面元素和model,也是angular雙向綁定機制的核心。
而ngmodel是angular用來處理表單(form)的最重要的指令,它連結了頁面表單中的可互動元素和位于$scope之上的model,它會自動把ngmodel所指向的model值渲染到form表單的可互動元素上,同時也會根據使用者在form表單的輸入或互動來更新此model值。
在源碼中,model值的格式化、解析、驗證都是由ngmodel指令所對應的控制器ngmodelcontroller來實作的。
在筆者所維護的國内ng群中,經常被問到一個問題:
對于ngmodel的這類問題主要分為兩類:
model值不滿足表單驗證條件,是以angular不會渲染它
由于javascript特殊的原型鍊繼承機制,對$scope中屬性的指派并不能更新到父$scope
在本節中,我們将會詳細分析此類問題,借此深入剖析ngmodel的工作原理。
我們先來看一個修改商品數量的例子,要求為必須輸入1-100的個數;
下面是對應的html代碼:
javascript代碼:
在代碼中我們已經為ngmodel變量amount指派了整數“0”,可是界面顯示效果仍然顯示”1 – 100”的placeholder(如下圖)。
下面是關于angular number元件ngmodel轉換函數代碼:
ngmodel作為angular雙向綁定中的重要組成部分,負責view控件互動資料到$scope上model的同步。當然這裡存在一些差異,view上的顯示和輸入都是字元串類型,而在model上的資料則是有特定類型的,如常用的int、float、date、array、object等。ngmodel為了實作資料到model的類型轉換,在ngmodelcontroller中提供了兩個管道數組$formatters和$parsers,它們分别是将model的資料轉換為view互動控件顯示的值和将互動控件得到的view值轉換為model資料,它們都是一個數組對象,在ngmodel啟動資料轉換時,會以unix管道式傳遞執行這一些列的轉換。我們也可以手動的添加$formatters和$parsers的轉換函數(unshift、push),當然在這裡也是做資料驗證的最佳時機,能夠轉換意味應該是合法的資料。
在number元件代碼中,我們清晰看見:依次添加了對數字驗證轉換、最小值合法性驗證、最大值合法驗證。首先會啟動$parsers轉換,如果在轉換過程中出現不合法驗證則會設定ngmodelcontroller.$setvalidity驗證錯誤,則傳回undefined。對于model資料到互動控件顯示,同樣也會經過$formatters轉換管道,對于沒有通過驗證的邏輯,同樣也會ngmodelcontroller.$setvalidity設定驗證錯誤,傳回undefined,是以這不合法的model資料不會顯示在互動控件上。
javascript中每個對象都會連結到一個原型對象,并且他可以從中繼承屬性。即使通過字面量建立的對象也會連結到object.prototype,它是javascript中的标配對象。javascript的原型鍊繼承相對于其他語言常見的繼承,是一種另類的繼承,它是實施于對象上的動态繼承方式,而非常見的實施與類型class之上的靜态繼承體系。javascript的這種繼承方式很靈活,一個對象可以被多個對象繼承,而且他們共享同一執行個體對象,但了解起來顯得格外複雜,從javascript原型和原型鍊可以看出它的複雜性。在javascript中,每個函數都有一個原型屬性prototype指向自身的原型,而由這個函數建立的對象也有一個proto屬性指向這個原型,而函數的原型是一個對象,是以這個對象也會有一個proto指向自己的原型,這樣逐層深入直到object對象的原型,這樣就形成了原型鍊。下面的是javascript原型繼承基礎原型和原型鍊展示圖。
函數是由function函數建立的對象,是以函數也有一個proto屬性指向function函數的原型。需要注意的是,真正形成原型鍊的是每個對象的proto屬性,而不是函數的prototype屬性。更多的内容關于原型和原型鍊的知識,請參考《javascript模式》這本書。
javascript的原型鍊連接配接隻在屬性檢索的時候才會啟用,如果我們嘗試去擷取對象的某個屬性值,但該對象沒有此屬性名,則javascript會試着從原型對象中擷取該屬性值。如果那個對象也沒有該屬性名,那麼在繼續從它的原型中尋找,依次類推,直到object.prototype,如果仍然沒有找到該屬性值,則傳回結果為undefined。不幸的是,這種原型鍊連接配接檢索,隻會在屬性檢索的的時候啟用,并不會在更新屬性值時啟用,是以當我們對于基礎類型(非引用對象上的屬性,換句通俗的話來說,就是不會出現“.”運算符)的屬性更新的時候,它并不能更新父對象的屬性,替代方式是在自身對象上建立了該屬性。這也是angular中對于基礎類型的屬性,不能在子controller中被修改的原因,導緻在子controller中ngmodel的更新并不會反應在父controller上。
下邊是關于該問題的一個簡化例子:
html:
javascript:
從初始化顯示效果中,我們能看出子$scope之繼承了來自父$scope的greet屬性,都顯示為”hello angular!“。如果我們嘗試利用父controller提供了input控件改變父$scope的greet屬性,你也能看見子controller區域的顯示也會被及時更新。對于ngcontroller預設會使用原型鍊繼承其父對象的屬性,所有的$scope的根$scope或稱祖$scope是來自ngapp節點建立的$rootscope,換句話說,$rootscope是萬物之源,所有的$scope都直接或者間接繼承至它。
當我們嘗試去改變輸入框的greet屬性的時,則發生了下面的情況:子controller區域發生了更新,父controller區域卻無法更新。因為上面所說的javascript的原型鍊檢索并不對更新啟用,對于基礎類型javascript在自身對象(這裡是子$scope)上建立了一個同名的變量。你也想可以從下面angular調試插件batarang截圖中看出來。一旦利用子controller的input控件修改了greet屬性,再次之後我再次嘗試修改父controller區域的greet屬性,子controller差別不會在像初始化時候那樣及時同步了,它們之間完全獨立了,各自擁有了自己的greet屬性。
batarang插件截圖
經過上面的例子分析,相信作為讀者的你已經能夠了解這類由于繼承鍊引用問題導緻的ngmodel不能更新問題了,請記住:這是javascript原型繼承的issue,并不是angular的issue。
那麼我們在子controller中如何更新父controller的屬性值呢?這個問題已經很簡單了,issue的問題在于沒有啟用原型鍊的檢索,那麼如果我們将ngmodel的屬性變為引用對象,換句話說:在ngmodel的屬性值中加了“.”,那麼在javascript的原型鍊檢索就會啟動了。
html:
javascript:
這裡在ngmodel屬性值多引入了“vm”變量,這個時候,不管我們嘗試修改greet值,整個頁面都會得到相應的同步。關于這個問題,作者更推薦使用angular 1.2後的controller as vm的方式解決,更多的資訊請閱讀《使用controller as vm方式.md》一節。