Angular 的雙向繫結與 AngularJS 的雙向繫結運作原理是完全不同的,目前看起來是沒有 AngularJS 會遇到效能問題。那 Angular 的雙向繫結到底是怎麼運作的呢?
如何使用 雙向繫結 (Two-way Binding)
以下的三種寫法都可以達到雙向繫結的效果
方法1
使用 [()] 的寫法
1 2 3
| <input [(ngModel)]="username">
<p>Hello {{username}}!</p>
|
方法2
將 [] () 分開寫
1 2 3
| <input [ngModel]="username" (ngModelChange)="username = $event">
<p>Hello {{username}}!</p>
|
方法3
不使用 ngModel
1 2 3
| <input [value]="username" (input)="username = $event.target.value">
<p>Hello {{username}}!</p>
|
[()] 的秘密
我們知道 [()] 是 Angular 所提供給雙向繫結的語法糖,但是底層到底是怎麼運作的,為什麼會可以轉換成 [<name>] + (<name>Change) 呢? 以下簡單說明
compiler/src/template_parser/template_parser.ts 裡面會去分析 Element 的 attribute 是否有符合各種格式的內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const BIND_NAME_REGEXP = /^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/;
const KW_BIND_IDX = 1;
const KW_LET_IDX = 2;
const KW_REF_IDX = 3;
const KW_ON_IDX = 4;
const KW_BINDON_IDX = 5;
const KW_AT_IDX = 6;
const IDENT_KW_IDX = 7;
const IDENT_BANANA_BOX_IDX = 8;
const IDENT_PROPERTY_IDX = 9;
const IDENT_EVENT_IDX = 10;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private _parseAttr( isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetEvents: BoundEventAst[], targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean { const name = this._normalizeAttributeName(attr.name); const value = attr.value; const srcSpan = attr.sourceSpan;
const bindParts = name.match(BIND_NAME_REGEXP); let hasBinding = false;
if (bindParts !== null) { hasBinding = true; ... } else if (bindParts[IDENT_BANANA_BOX_IDX]) { this._bindingParser.parsePropertyBinding( bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps); this._parseAssignmentEvent( bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
} ... return hasBinding; }
|
-
根據 _parseAssigmentEvent 就會將部分[(ngModel)]="username" 轉換成 (ngModelChange)="username = $event" 傳入 bindingParser.parseEvent 的方法內
1 2 3 4 5 6
| private _parseAssignmentEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { this._bindingParser.parseEvent( `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents); }
|
-
this._bindingParse.parseEvent,會更新 Element 的屬性值
1 2 3 4 5 6 7 8 9 10 11
| private _parseEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { const [target, eventName] = splitAtColon(name, [null !, name]); const ast = this._parseAction(expression, sourceSpan); targetMatchableAttrs.push([name !, ast.source !]); targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan)); }
|
-
這就是 [()] 語法糖的運作方式
ngModel
ngModel 是 Angular 所提供的 Directive,主要用途是用來簡化雙向繫結的寫法,程式碼可以參閱這裡
程式碼說明
ngOnChanges
第一次 Input Change 時,註冊 Control 等相關事件,註冊流程如下
- 檢查是否有註冊過,如果沒有,執行
_setUpControl 的方法,setUpControl是在 ./shared.ts 內實作的,主要功能是 Control 的事件註冊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ngOnChanges(changes: SimpleChanges) { this._checkForErrors(); if (!this._registered) this._setUpControl(); if ('isDisabled' in changes) { this._updateDisabled(changes); }
if (isPropertyUpdated(changes, this.viewModel)) { this._updateValue(this.model); this.viewModel = this.model; } } ... private _setUpControl(): void { this._isStandalone() ? this._setUpStandalone() : this.formDirective.addControl(this); this._registered = true; }
private _isStandalone(): boolean { return !this._parent || !!(this.options && this.options.standalone); }
private _setUpStandalone(): void { setUpControl(this._control, this); this._control.updateValueAndValidity({emitEvent: false}); }
|
setUpControl 內有許多事件註冊行為,而跟 two-way binding 有關的事件是 dir.valueAccessor!.registerOnChange,這裡會傳入一個 callback function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function setUpControl(control: FormControl, dir: NgControl): void { ... setUpViewChangePipeline(control, dir); ... } function setUpViewChangePipeline(control: FormControl, dir: NgControl): void { dir.valueAccessor !.registerOnChange((newValue: any) => { control._pendingValue = newValue; control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir); }); } function updateControl(control: FormControl, dir: NgControl): void { dir.viewToModelUpdate(control._pendingValue); if (control._pendingDirty) control.markAsDirty(); control.setValue(control._pendingValue, {emitModelToViewChange: false}); }
|
- 而當 Input 欄位有資料輸入時,就會觸發事件並將回傳值發送到到頁面上
ng_model.ts
1 2 3 4
| viewToModelUpdate(newValue: any): void { this.viewModel = newValue; this.update.emit(newValue); }
|
NG_VALUE_ACCESSOR
這個 provider 是讓 ngModleChange 接受 $event 而不是 $event.target.value 的魔法使,內部細節如下

在各類型的 Control 都會有一份 NG_VALUE_ACCESSOR ,而針對 ngModel 我們需留意的是 DEFAULT_VALUE_ACCESSOR ,檔案是 default_value_accessor.ts
(使用 multi 的 DI 設定方式並不是這篇文章的重點,只要知道這樣子設定,可以讓 Provider 使用同一個名稱但又可同時存在不互相影響)
1 2 3 4 5
| export const DEFAULT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DefaultValueAccessor), multi: true };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @Directive({ selector: 'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]', host: { '(input)': '_handleInput($event.target.value)', '(blur)': 'onTouched()', '(compositionstart)': '_compositionStart()', '(compositionend)': '_compositionEnd($event.target.value)' }, providers: [DEFAULT_VALUE_ACCESSOR] }) export class DefaultValueAccessor implements ControlValueAccessor { onChange = (_: any) => {}; onTouched = () => {};
private _composing = false;
constructor( private _renderer: Renderer, private _elementRef: ElementRef, @Optional() @Inject(COMPOSITION_BUFFER_MODE) private _compositionMode: boolean) { if (this._compositionMode == null) { this._compositionMode = !_isAndroid(); } }
writeValue(value: any): void { const normalizedValue = value == null ? '' : value; this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue); }
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }
...
_handleInput(value: any): void { if (!this._compositionMode || (this._compositionMode && !this._composing)) { this.onChange(value); } }
... }
|
DefaultValueAccessor 裡 registerOnChange 與 onChange 的關係是,ngModel 會經 setUpControl 的方法將自訂方法透過 registerOnChange 註冊到 onChange 上,
DefaultValueAccessor 的 @Directive 的宣告的地方,有註冊 (input) 事件發生時會觸發的方法, _handleInput($event.target.value)
1 2 3 4 5
| _handleInput(value: any): void { if (!this._compositionMode || (this._compositionMode && !this._composing)) { this.onChange(value); } }
|
經過這一串的折騰,魔法就出現了,ngModle 的 @Output('ngModelChange') 會收到並發送資料到頁面上,這也就是為什麼 (ngModelChange) 的 $event 不需要加上 target.value,又可以取得異動的資料
Recap
以下是雙向繫結相關的流程順序
[ngModel]時會觸發 ngOnChanges 事件
- 在
ngOnChanges 時,會執行 setUpControl() 方法
- 在
setupControl() 內會註冊 DefaultValueAccess 執行 registerOnChange,並將 callback function 傳入
- 透過
registerOnChanges 傳入的 callback function 會被綁定到 onChanges 上
- 當
(input) 事件被觸發時,會執行 _handleInput($event.target.value) 的方法
- 將傳入
_handleInput(value) 的值傳給註冊在 onChange 的 callback function
- callback function 會執行
ngModel 裡的 viewToModelUpdate(newValue) 方法
- 最後將
viewToModelUpdate 所接受到的值,透過 ngModelChange 的 EventEmiiter emit 值到頁面上
- 完成整個雙向繫結的動作