angular html原理,Angular 4.x ngModel 双向绑定原理揭秘

在 Angular 4.x 中对于使用 Template-Driven 表单场景,如果需要实现表单数据绑定。我们就需要引入 ngModel 指令。该指令用于基于 domain 模型,创建 FormControl 实例,并将创建的实例绑定到表单控件元素上。

ngModel 使用示例

ngModel

app.component.ts

@Component({

selector: 'exe-app',

template: `

Name:

{{ f.value | json }}

`,

})

export class AppComponent implements OnInit { }

表单中使用 ngModel 时,我们需要设置一个 name 属性,以便该控件可以使用该名称在表单中进行注册。

单向绑定 - [ngModel]

app.component.ts

@Component({

selector: 'exe-app',

template: `

Name:

{{ user | json }}

`,

})

export class AppComponent implements OnInit {

user: { username: string };

ngOnInit() {

this.user = { username: 'Semlinker' };

}

}

双向绑定 - [(ngModel)]

表单中应用

app.component.ts

@Component({

selector: 'exe-app',

template: `

Name:

{{ user | json }}

`,

})

export class AppComponent implements OnInit {

user: { username: string };

ngOnInit() {

this.user = { username: 'Semlinker' };

}

}

单独应用

import { Component } from '@angular/core';

@Component({

selector: 'exe-app',

template: `

{{username}}

`,

})

export class AppComponent {

username: string;

}

ngModelOptions - [ngModelOptions]

当你在使用 ngModel 时未设置 name 属性,如下所示:

Name:

当你运行时,浏览器控制台将会抛出以下异常信息:

Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.

以上异常信息告诉我们,如果在表单标签中使用 ngModel,则必须设置 name 属性,或者在 ngModelOptions 中必须将表单控件定义为 "standalone"。依据上述异常信息,我们做如下调整:

Name:

[ngModelOptions]="{standalone: true}">

接下来我们看一下 ngModelOptions 支持的对象类型:

@Input('ngModelOptions') options: {name?: string, standalone?: boolean};

禁用控件 - disabled

Name:

[(ngModel)]="user.username" disabled="true">

监听 ngModelChange 事件 - (ngModelChange)

app.component.ts

@Component({

selector: 'exe-app',

template: `

Name:

[(ngModel)]="user.username">

{{ user | json }}

`,

})

export class AppComponent implements OnInit {

user: { username: string };

ngOnInit() {

this.user = { username: 'Semlinker' };

}

userNameChange(name: string) {

console.log(name);

}

}

获取关联的 NgModel 对象

app.component.ts

@Component({

selector: 'exe-app',

template: `

Name:

[(ngModel)]="user.username">

{{ userName.control | json }}

`,

})

export class AppComponent implements OnInit {

user: { username: string };

ngOnInit() {

this.user = { username: 'Semlinker' };

}

}

通过使用 userName="ngModel" 方式,我们可以获取表单控件关联的 NgModel 对象,进而获取控件当前控件的相关信息,如控件的当前的状态或控件验证信息等。

完整示例

import { Component } from '@angular/core';

import { NgForm } from '@angular/forms';

@Component({

selector: 'exe-app',

template: `

Submit

First name value: {{ first.value }}

First name valid: {{ first.valid }}

Form value: {{ f.value | json }}

Form valid: {{ f.valid }}

`,

})

export class AppComponent {

onSubmit(f: NgForm) {

console.log(f.value); // { first: '', last: '' }

console.log(f.valid); // false

}

}

ngModel 指令详解

ngModel 指令定义

@Directive({

selector: '[ngModel]:not([formControlName]):not([formControl])',

providers: [formControlBinding],

exportAs: 'ngModel'

})

formControlBinding 定义

export const formControlBinding: any = {

provide: NgControl,

useExisting: forwardRef(() => NgModel)

};

相关说明

selector 中 [ngModel]:not([formControlName]):not([formControl]) 表示该指令只应用于 Template-Driven 表单中。

exportAs - 表示可以使用 first="ngModel" 语法获取 NgModel 对象

ngModel 指令输入与输出属性

输入属性

@Input() name: string;

@Input('disabled') isDisabled: boolean;

@Input('ngModel') model: any;

@Input('ngModelOptions') options: {name?: string, standalone?: boolean};

输出属性

@Output('ngModelChange') update = new EventEmitter();

NgModel 类

// angular2/packages/forms/src/directives/ng_model.ts

export class NgModel extends NgControl implements OnChanges,

OnDestroy {

/** @internal */

_control = new FormControl(); // 创建FormControl对象

/** @internal */

_registered = false; // 用于标识控件是否已注册

viewModel: any; // 用于保存前一次model的值

...

}

NgModel 构造函数

constructor(

@Optional() @Host() parent: ControlContainer,

@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array,

@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:

Array,

@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)

valueAccessors: ControlValueAccessor[]) {

super();

this._parent = parent;

this._rawValidators = validators || [];

this._rawAsyncValidators = asyncValidators || [];

this.valueAccessor = selectValueAccessor(this, valueAccessors);

}

相关说明

@Optional() - 表示该依赖对象是可选的

@Host() - 表示从宿主元素注入器获取依赖对象

@Self() - 表示从当前注入器获取依赖对象

@Inject() - 用于注入 Token (new InjectionToken) 对应的非 Type 类型依赖对象

构造函数执行的操作:

获取 ControlContainer (控件容器)对象

获取控件上的同步验证器

获取控件上的异步验证器

获取控件上的 ControlValueAccessor

NgModel 生命周期钩子

ngOnChanges

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;

}

}

_checkForErrors()

private _checkForErrors(): void {

if (!this._isStandalone()) {

this._checkParentType();

}

this._checkName();

}

// 判断是否设置standalone属性

private _isStandalone(): boolean {

return !this._parent || (this.options && this.options.standalone);

}

/**

* 1.ngModel指令不能与formGroupName或formArrayName指令一起使用,需改用

* formControlName或调整ngModel的父控件使用的指令为ngModelGroup。

*

* 2.ngModel不能被注册到使用formGroup指令的表单中,需改用formControlName或设置

* ngModelOptions对象中的standalone属性,避免注册该控件。

*/

private _checkParentType(): void {

if (!(this._parent instanceof NgModelGroup) &&

this._parent instanceof AbstractFormGroupDirective) {

TemplateDrivenErrors.formGroupNameException();

} else if (!(this._parent instanceof NgModelGroup) &&

!(this._parent instanceof NgForm)) {

TemplateDrivenErrors.modelParentException();

}

}

/**

* 验证是否设置name属性

*

* 如果在表单标签中使用 ngModel,则必须设置 name 属性,或者在ngModelOptions中必须将

* 表单控件定义为"standalone"。

*

*

* true}">

*/

private _checkName(): void {

if (this.options && this.options.name) this.name = this.options.name;

if (!this._isStandalone() && !this.name) {

TemplateDrivenErrors.missingNameException();

}

}

_setUpControl()

// 初始化控件

private _setUpControl(): void {

this._isStandalone() ? this._setUpStandalone() :

// 在ControlContainer所属的form中注册该控件

this.formDirective.addControl(this);

this._registered = true; // 标识已注册

}

// 若设置standalone属性,则初始化该控件,并更新控件的值和验证状态

private _setUpStandalone(): void {

setUpControl(this._control, this);

this._control.updateValueAndValidity({emitEvent: false});

}

// 获取ControlContainer所属的form

get formDirective(): any {

return this._parent ? this._parent.formDirective : null;

}

_updateDisabled()

若设置 isDisabled 输入属性,则更新控件的 disabled 属性:

// 更新控件的disabled状态

private _updateDisabled(changes: SimpleChanges) {

// 获取disabled输入属性的当前值

const disabledValue = changes['isDisabled'].currentValue;

// 判断是否设置为disabled

const isDisabled = disabledValue === '' ||

(disabledValue && disabledValue !== 'false');

resolvedPromise.then(() => {

if (isDisabled && !this.control.disabled) {

this.control.disable(); // 禁用控件

} else if (!isDisabled && this.control.disabled) {

this.control.enable(); // 启用控件

}

});

}

isPropertyUpdated()

// 判断属性是否更新

export function isPropertyUpdated(changes: {[key: string]: any},

viewModel: any): boolean {

if (!changes.hasOwnProperty('model')) return false; // @Input('ngModel') model: any;

const change = changes['model'];

if (change.isFirstChange()) return true; // 判断是否首次改变

return !looseIdentical(viewModel, change.currentValue);

}

// JS has NaN !== NaN

export function looseIdentical(a: any, b: any): boolean {

return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a)

&& isNaN(b);

}

_updateValue()

// 更新控件的值

private _updateValue(value: any): void {

resolvedPromise.then(

() => { this.control.setValue(value, {emitViewToModelChange: false});

});

}

const resolvedPromise = Promise.resolve(null);

ngOnDestroy()

// 指令销毁时,从formDirective中移除该控件

ngOnDestroy(): void {

this.formDirective && this.formDirective.removeControl(this);

}

NgModel 方法

get control(): FormControl

// 获取控件

get control(): FormControl { return this._control; }

/** @internal */

_control = new FormControl();

get path(): string[]

// 获取控件的访问路径

get path(): string[] {

return this._parent ? controlPath(this.name, this._parent) : [this.name];

}

get validator(): ValidatorFn

// 获取同步验证器

get validator(): ValidatorFn {

return composeValidators(this._rawValidators);

}

export interface ValidatorFn { (c: AbstractControl): ValidationErrors|null; }

get asyncValidator(): AsyncValidatorFn

// 获取异步验证器

get asyncValidator(): AsyncValidatorFn {

return composeAsyncValidators(this._rawAsyncValidators);

}

export interface AsyncValidatorFn {

(c: AbstractControl): Promise|Observable;

}

viewToModelUpdate(newValue: any): void

// 触发ngModelChange事件

viewToModelUpdate(newValue: any): void {

this.viewModel = newValue;

// @Output('ngModelChange') update = new EventEmitter();

this.update.emit(newValue);

}

NgControl 抽象类

// angular2/packages/forms/src/directives/ng_control.ts

// 所有控件指令都需继承的基类,绑定FormControl对象至DOM元素

export abstract class NgControl extends AbstractControlDirective {

/** @internal */

_parent: ControlContainer = null;

name: string = null;

valueAccessor: ControlValueAccessor = null;

/** @internal */

_rawValidators: Array = [];

/** @internal */

_rawAsyncValidators: Array = [];

get validator(): ValidatorFn { return unimplemented(); }

get asyncValidator(): AsyncValidatorFn { return unimplemented(); }

abstract viewToModelUpdate(newValue: any): void;

}

AbstractControlDirective 抽象类

// angular2/packages/forms/src/directives/abstract_control_directive.ts

export abstract class AbstractControlDirective {

// 获取控件

get control(): AbstractControl { throw new Error('unimplemented'); }

// 获取控件的值

get value(): any { return this.control ? this.control.value : null; }

// 控件控件的验证状态 - valid、invalid、pending

get valid(): boolean { return this.control ? this.control.valid : null; }

get invalid(): boolean { return this.control ? this.control.invalid : null; }

get pending(): boolean { return this.control ? this.control.pending : null; }

get pristine(): boolean { return this.control ? this.control.pristine : null; }

get dirty(): boolean { return this.control ? this.control.dirty : null; }

get touched(): boolean { return this.control ? this.control.touched : null; }

get untouched(): boolean { return this.control ? this.control.untouched : null; }

get disabled(): boolean { return this.control ? this.control.disabled : null; }

get enabled(): boolean { return this.control ? this.control.enabled : null; }

// 获取控件验证异常对象

get errors(): ValidationErrors|null {

return this.control ? this.control.errors : null;

}

// 获取statusChanges对象

get statusChanges(): Observable {

return this.control ? this.control.statusChanges : null;

}

// 获取valueChanges对象

get valueChanges(): Observable {

return this.control ? this.control.valueChanges : null;

}

// 获取控件路径

get path(): string[] { return null; }

// 重设控件的值

reset(value: any = undefined): void {

if (this.control) this.control.reset(value);

}

// 判断是否path路径对应的控件,是否存在errorCode对应的错误

hasError(errorCode: string, path: string[] = null): boolean {

return this.control ? this.control.hasError(errorCode, path) : false;

}

// 获取path路径对应的控件,参数errorCode对应的错误

getError(errorCode: string, path: string[] = null): any {

return this.control ? this.control.getError(errorCode, path) : null;

}

}

input 指令

input 指令定义

@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]

})

相关说明

compositionstart - 事件触发于一段文字的输入之前 (类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。

compositionend - 事件触发于完成文本段落输入或取消输入

compositionstart、compositionend 的实际应用,请参考 - 应对中文输入法的字符串截断方案

DEFAULT_VALUE_ACCESSOR

export const DEFAULT_VALUE_ACCESSOR: any = {

provide: NG_VALUE_ACCESSOR,

useExisting: forwardRef(() => DefaultValueAccessor),

multi: true

};

DefaultValueAccessor

export class DefaultValueAccessor implements ControlValueAccessor {

onChange = (_: any) => {};

onTouched = () => {};

/** Whether the user is creating a composition string (IME events). */

private _composing = false;

constructor(

private _renderer: Renderer, // 注入Renderer对象

private _elementRef: ElementRef,

@Optional() @Inject(COMPOSITION_BUFFER_MODE)

private _compositionMode: boolean) {

if (this._compositionMode == null) {

this._compositionMode = !_isAndroid();

}

}

// 将模型中的新值写入视图或DOM元素属性中

writeValue(value: any): void {

const normalizedValue = value == null ? '' : value;

this._renderer.setElementProperty(this._elementRef.nativeElement,

'value', normalizedValue);

}

// 设置当控件接收到change事件后,调用的函数

registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }

// 设置当控件接收到touched事件后,调用的函数

registerOnTouched(fn: () => void): void { this.onTouched = fn; }

// 设置控件的Disabled状态

setDisabledState(isDisabled: boolean): void {

this._renderer.setElementProperty(this._elementRef.nativeElement,

'disabled', isDisabled);

}

// 处理input事件

_handleInput(value: any): void {

if (!this._compositionMode || (this._compositionMode && !this._composing)) {

this.onChange(value);

}

}

// 处理compositionstart事件

_compositionStart(): void { this._composing = true; }

// 处理compositionend事件

_compositionEnd(value: any): void {

this._composing = false;

this._compositionMode && this.onChange(value);

}

}

export const COMPOSITION_BUFFER_MODE = new InjectionToken

('CompositionEventMode');

// 用于判断是否处于安卓平台,composition事件在iOS和Android存在兼容性

function _isAndroid(): boolean {

const userAgent = getDOM() ? getDOM().getUserAgent() : '';

return /android (\d+)/.test(userAgent.toLowerCase());

}

相关说明

为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。

另外看完上面的代码,不知道读者有没有以下的疑问:

writeValue() 方法什么时候调用?

registerOnChange() 什么时候调用?

registerOnTouched() 什么时候调用?

为了解开这些疑惑我们就需要分析一下,一个很重要的方法 - setUpControl()。我们先来看一下 setUpControl() 的调用的时机点:

NgModel ngOnChanges 生命周期钩子

ngOnChanges(changes: SimpleChanges) {

...

if (!this._registered) this._setUpControl();

...

}

_setUpControl() 方法

private _setUpControl(): void {

this._isStandalone() ? this._setUpStandalone() :

// 在ControlContainer所属的form中注册该控件

this.formDirective.addControl(this);

this._registered = true; // 标识已注册

}

_setUpControl() 方法内部,先判断控件有设置 standalone 属性,如果有的话,则调用 _setUpStandalone() 方法:

// 若设置standalone属性,则初始化该控件,并更新控件的值和验证状态

private _setUpStandalone(): void {

setUpControl(this._control, this); // 调用时机点一

this._control.updateValueAndValidity({emitEvent: false});

}

如果没有设置 standalone 属性,则调用 this.formDirective.addControl(this),这个方法存在于我们的 form 指令中,我们直接看一下具体实现:

addControl(dir: NgModel): void {

resolvedPromise.then(() => {

const container = this._findContainer(dir.path);

dir._control = container.registerControl(dir.name, dir.control);

setUpControl(dir.control, dir); // 调用时机点二

dir.control.updateValueAndValidity({emitEvent: false});

});

}

搞清楚 setUpControl() 调用的时机点,是时候分析一下 setUpControl() 方法的具体实现了。

setUpControl()

// angular2/packages/forms/src/directives/shared.ts

export function setUpControl(control: FormControl, dir: NgControl): void {

if (!control) _throwError(dir, 'Cannot find control with');

/**

* NgModel构造函数

* @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]

* this.valueAccessor = selectValueAccessor(this, valueAccessors);

*/

// 判断控件是否实现ControlValueAccessor接口

if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');

// 组合同步验证器

control.validator = Validators.compose([control.validator, dir.validator]);

// 组合异步验证器

control.asyncValidator = Validators.composeAsync([control.asyncValidator,

dir.asyncValidator]);

// 该方法用于将模型中的新值写入视图或 DOM 属性中

dir.valueAccessor.writeValue(control.value);

// view -> model

/**

* @Directive({

* selector: 'input:not([type=checkbox])[formControlName],...',

* host: {

* '(input)': '_handleInput($event.target.value)'

* },

* providers: [DEFAULT_VALUE_ACCESSOR]

* })

* export class DefaultValueAccessor implements ControlValueAccessor {

* // 下面就是调用该方法

* registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }

*

* // input事件触发后,调用该方法

* _handleInput(value: any): void {

* if (!this._compositionMode || (this._compositionMode && !this._composing)) {

* this.onChange(value); //调用下面注册的onChange函数

* }

* }

* }

*

*/

dir.valueAccessor.registerOnChange((newValue: any) => {

/**

* ngModel指令 - viewToModelUpdate() 方法

*

* viewToModelUpdate(newValue: any): void {

* this.viewModel = newValue; // 更新viewModel

* // @Output('ngModelChange') update = new EventEmitter();

* this.update.emit(newValue); // 触发ngModelChange事件

* }

*/

dir.viewToModelUpdate(newValue);

control.markAsDirty();

/*

* setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange,

* emitViewToModelChange}: {

* onlySelf?: boolean,

* emitEvent?: boolean,

* emitModelToViewChange?: boolean,

* emitViewToModelChange?: boolean

* } = {}): void {

* this._value = value;

* if (this._onChange.length && emitModelToViewChange !== false) {

* this._onChange.forEach((changeFn) => changeFn(this._value,

* emitViewToModelChange !== false));

* }

* this.updateValueAndValidity({onlySelf, emitEvent});

* }

*/

control.setValue(newValue, {emitModelToViewChange: false}); // 更新控件的值

});

// touched

dir.valueAccessor.registerOnTouched(() => control.markAsTouched());

/**

* control = new FormControl();

*

* control - _onChange 属性

* _onChange: Function[] = [];

*

* control - registerOnChange() 方法

* registerOnChange(fn: Function): void { this._onChange.push(fn); }

*/

control.registerOnChange((newValue: any, emitModelEvent: boolean) => {

// control -> view

/*

* writeValue(value: any): void {

* const normalizedValue = value == null ? '' : value;

* this._renderer.setElementProperty(this._elementRef.nativeElement, 'value',

* normalizedValue);

* }

*/

dir.valueAccessor.writeValue(newValue);

// control -> ngModel

/**

* ngModel指令 - viewToModelUpdate() 方法

*

* viewToModelUpdate(newValue: any): void {

* this.viewModel = newValue; // 更新viewModel

* // @Output('ngModelChange') update = new EventEmitter();

* this.update.emit(newValue); // 触发ngModelChange事件

* }

*/

if (emitModelEvent) dir.viewToModelUpdate(newValue);

});

// 当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数

// 值,启用或禁用指定的 DOM 元素

if (dir.valueAccessor.setDisabledState) {

control.registerOnDisabledChange(

(isDisabled: boolean) => { dir.valueAccessor.setDisabledState(isDisabled); });

}

// re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4

dir._rawValidators.forEach((validator: Validator | ValidatorFn) => {

if ((validator).registerOnValidatorChange)

(validator).registerOnValidatorChange(() =>

control.updateValueAndValidity());

});

dir._rawAsyncValidators.forEach((validator: AsyncValidator | AsyncValidatorFn) => {

if ((validator).registerOnValidatorChange)

(validator).registerOnValidatorChange(() =>

control.updateValueAndValidity());

});

}

最后我们再看一下 ControlValueAccessor 接口:

ControlValueAccessor

// angular2/packages/forms/src/directives/control_value_accessor.ts

export interface ControlValueAccessor {

writeValue(obj: any): void;

registerOnChange(fn: any): void;

registerOnTouched(fn: any): void;

setDisabledState?(isDisabled: boolean): void;

}

writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中

registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数

registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数

setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素

了解 ControlValueAccessor 的详细信息,可以参考 - Understanding ControlValueAccessor

明天补充图示说明哈,能够理解的同学请直接略过。

参考资源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值