咱們知道,Angular 的 @angular/forms 包提供了 NgModel 指令,來實現雙向綁定,即把一個 JS 變量(假設爲 name)與一個 DOM 元素(假設爲 input 元素)進行綁定,這樣 name 的值發生變化,input 元素 的 value 也會自動變化;input 元素的 value 發生變化,name 的值也會自動變化。以下代碼,展現一個最簡單的雙向綁定(也可見 stackblitz demo):css
@Component({ selector: 'my-app', template: ` <input [ngModel]="name" (ngModelChange)="this.name=$event"> <button (click)="this.name = this.name + ' , apple';">ChangeName</button> <p>{{name}}</p> `, styleUrls: [ './app.component.css' ] }) export class AppComponent { name = 'banana'; }
上面代碼使用了 NgModel 指令來把變量 name 和 input DOM 元素雙向綁定到了一塊兒,這裏爲了更清晰理解 NgModel 的本質,沒有使用 [(ngModel)] 語法糖。實際上,在模板裏寫 [(xxx)] 這種 'BANANA_BOX' 語法,@angular/compiler 的 Template Parser 會把這種語法拆解爲爲 [xxx] 和 (xxxChange),可看 L448-L453 和 L501-L505,因此 [(xxx)] 僅僅是爲了省事的簡單寫法。html
查看 stackblitz demo 能夠看到,若是修改 input 裏的值,name 變量的值也自動發生變化了,這點可從與 name 綁定的 p 標籤值自動變化看出;若是點擊 button 修改了 name 的值,input 輸入框內的 value 值也發生變化了,這點可從 input 框內的值變化可看到。那 NgModel 指令是如何作到雙向綁定的呢?react
在理解 NgModel 指令雙向綁定原理以前,能夠先看看雙向綁定最簡單形式:git
<input [value]="country" (input)="country = $event.target.value"> <button (click)="country = country + ' , China';">ChangeCountry</button> <p>Hello {{country}}!</p>
點擊 button 修改 model 時,就會自動修改 input 的 value 值,即自動修改 view,數據流方向就是 model -> view;更新 input 框內值時,就會自動修改 country 這個 model 值,這點可從綁定的 p 標籤看到,這時數據流方向就是 view -> model。固然,這是最簡單且最不可擴展的一個雙向綁定實例,若是去設計一個指令,不單單須要考慮 view 的不一樣類型,並且還須要考慮數據校驗問題。儘管如此,這個簡單實例與 NgModel 指令本質是相似的。github
若是本身設計這樣一個雙向綁定指令,那它的輸入必然是綁定的變量 name,該指令接收 name 後再去更新 input 元素的 value 值(還得支持 textarea,select 等 DOM 元素,甚至組件等自定義 DOM 元素),這樣 name 發生變化,input 的 value 也會自動變化,即 model -> view;輸出的必然是 input 元素的 value 值,而後賦值給 name,這樣 input 元素的值變化,name 值也自動變化,即 view -> model。這裏的最難點是該指令得可以寫 DOM 元素(無論原生或者自定義 DOM 元素)的值,而且可以監聽 DOM 元素的值變化,讀取變化的值。 因此,爲了支持原生 DOM 元素或自定義 DOM 元素,爲了有個好的設計模式,必然會抽象出一個接口,來幫助指令去寫入和監聽讀取 DOM 元素值,有了這個接口,事情就簡單不少了。設計模式
如今,咱們須要搞明白兩個問題:name 值發生變化時,input 的 value 如何自動變化;input 的 value 變化,name 值如何自動變化?數組
綁定到 input 上的 NgModel 指令在實例化時,其 構造函數 會首先查找出 ControlValueAccessor 對象,這個 ControlValueAccessor 就是上文提到的抽象出來的對象,該對象會具體負責更新和監聽讀取 DOM 元素的值。上文模板中的 input 元素不單單綁定了 NgModel 指令,實際上還綁定了 DefaultValueAccessor 指令,這點能夠從該指令的選擇器知道,若是 input 模板是這麼寫的:app
<input [ngModel]="name" (ngModelChange)="this.name=$event" type="number">
那不單單綁定了 DefaultValueAccessor 指令,還綁定了 NumberValueAccessor 指令。ide
因爲 DefaultValueAccessor 的 providers 屬性提供了 NG_VALUE_ACCESSOR 令牌,而且該令牌指向的對象就是 DefaultValueAccessor,因此 NgModel 構造函數中注入的 NG_VALUE_ACCESSOR 令牌包含的 ControlValueAccessor 對象數組只有 DefaultValueAccessor 一個。若是是 type="number" 的 input,則 valueAccessors 包含 NumberValueAccessor 和 DefaultValueAccessor 這兩個對象。構造函數中的 selectValueAccessor() 方法會依次遍歷 NG_VALUE_ACCESSOR 令牌提供的 ControlValueAccessor 對象數組,若是是自定義的 ControlValueAccessor 優先選擇自定義的,若是是 @angular/forms 內置的 ControlValueAccessor 就選擇內置的(內置的也就 6 個),不然最後選擇默認的 ControlValueAccessor 即 DefaultValueAccessor 對象。對於本文 demo,那就是默認的 DefaultValueAccessor 對象。注意的一點是,注入的 NG_VALUE_ACCESSOR 令牌有裝飾器 @Self,因此只能從自身去查找這個依賴,自身的意思是 NgModel 指令本身,和它一塊兒掛載到 input 元素的其餘指令。另外,input 上沒有綁定任何 validators 指令,因此注入的 NG_VALIDATORS 和 NG_ASYNC_VALIDATORS 令牌解析的值爲空,而且 input 單獨使用,沒有放在 form 元素內,或 FormGroup 綁定的元素內,因此不存在宿主控件容器 ControlContainer,即 parent 也爲空。函數
NgModel 指令在首次實例化時,運行 _setUpControl() 方法,利用 ControlValueAccessor(本 demo 即 DefaultValueAccessor 對象) 把 NgModel 指令內部的 FormControl 對象與 DOM 元素綁定。因爲本 demo 中,NgModel 指令綁定的 input 沒有父控件容器,因此會調用 _setUpStandalone 方法,核心方法就是 setUpControl(),該方法主要包含兩點:第一點,經過調用 setUpViewChangePipeline() 向 DefaultValueAccessor 對象內註冊一個回調函數,這樣當 input 值發生變化時,就觸發 input 事件 時,會執行這個回調函數,而這個回調函數的邏輯 一是更新 FormControl 的 value,二是讓 NgModel 指令拋出 ngModelChange 事件,該事件包含的值就是當前 input 變化的新值,因此,setUpViewChangePipeline() 方法的做用就是搭建了 view -> model 的管道,這樣 view (這裏是 input) 值發生變化時,會同步 FormControl 對象的 value 值,並讓 NgModel 指令把這個新值輸出出去;第二點,經過調用 setUpModelChangePipeline 方法向 FormControl 對象內註冊 一個回調,這個回調邏輯是當 FormControl 的 value 值發生變化時(本 demo 中就是 [ngModel]="name" 時,name 值發生變化,也就是屬性值改變,這樣 isPropertyUpdated(changes, this.viewModel) 就爲 true,這樣就會須要更新 FormControl 的 value 值 FormControl.setValue(value),從而會 觸發 上文說的 FormControl 對象內的回調函數),經過調用 ControlValueAccessor.writeValue() 方法去修改 view (這裏是 input) 的 value 值(本 demo 中使用的是 DefaultValueAccessor.writeValue(value)),而後讓 NgModel 指令拋出 ngModelChange 事件,該事件包含的值就是當前 FormControl 對象 變化的新值,因此,setUpModelChangePipeline() 方法的做用就是搭建了 model -> view 的管道,這樣 FormControl 對象值發生改變時,會同步更新 view 的 value,並讓 NgModel 指令把這個新值輸出出去。
經過以上的解釋,就能理解 name 值發生變化時,input 的 value 是如何自動變化的;input 的 value 發生變化時,name 值是如何自動變化的。(最好能一個個點擊連接查看源碼,效率更高。) 一句話解釋就是:NgModel 指令初始化時先安裝了兩個回調(一個是 view 變化時更新 FormControl 對象 value 值的回調,另外一個是 FormControl 對象 value 值變化時更新 view 值的回調),數據流方向從 view -> model 時,更新 FormControl 對象並拋出攜帶該值的 ngModelChange 事件,數據流方向從 model -> view 時,利用 ControlValueAccessor 去更新 view 值,同時也拋出攜帶該值的 ngModelChange 事件。拋出的 ngModelChange 事件包含新值,模板中的 $event 會被 @angular/compiler 特殊處理,爲 ngModelChange 事件拋出的值。
固然,本文沒有考慮存在 Validators 的狀況,若是 input 模板修改成以下代碼:
<input [ngModel]="name" (ngModelChange)="this.name=$event" required>
那該模板除了綁定 NgModel 指令外,還綁定了 RequiredValidator 指令,這樣無論數據流方向是 view -> model 仍是 model -> view,在數據流動以前,還須要運行驗證器,驗證數據的有效性。這樣 NgModel 的構造函數裏就會包含 一個 RequiredValidator 對象,而後 把這個 Validator 傳給 FormControl 對象,最後註冊 validatorChange 回調,這樣之後 FormControl 值更新時就會 運行 Validators。
總之,NgModel 指令來管理 model <-> view 的數據流,內部存在一個 FormControl 對象,用來讀取存儲值和驗證有效性,從 FormControl 讀取的值會賦值給外界傳進來的 model,view 是藉助 ControlValueAccessor 來讀寫值。整個 @angular/forms 包的設計也是按照這種數據流形式,並不複雜。
也可閱讀 @angular/forms 相關文章瞭解如何寫一個自定義的 ControlValueAccessor:譯 別再對 Angular 表單的 ControlValueAccessor 感到迷惑。