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

转自:https://segmentfault.com/a/1190000009126012

在 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 支持的对象类型:

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

禁用控件 - disabled

Name:

监听 ngModelChange 事件 - (ngModelChange)
app.component.ts

@Component({
  selector: 'exe-app',
  template: `
   
Name:
{{ 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:
{{ 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: `
    

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 构造函数
c

onstructor(
  @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"。
 *
 * 
 */
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 等。

了解详细的信息,请查看 - Angular 2 ElementRef

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

  • 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

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

参考资源
Input Method Editor API

你可能感兴趣的:(angular)