Angular Form 的数据流

Angular Form 总观

先给总结,再谈细节

  • ReactForm, Template driven Form 的差异:
    • ReactForm: 需要我们自行定义 FormControl,适用于数据结构不变,验证很方便,数据流刷新时同步的。
    • Template driven Form: 不需要我们自行定义 FormControl, 适用于数据结构易变,数据流同步,从 ts 到 Dom 是异步的,Dom 到 ts 是同步的。验证需要我们写 Directive.
  • 我们可以通过自己实现ControlValueAccessor,来实现一个自定义的 Input, 使用的时候更HTMLInputElement类似。
  • ControlValueAccessor可以级联。

Form 的两种使用场景与区别

React FormTemplate Form,使用的例子如下,这个是官方用例
下面的是三个React Form的使用场景

// React Form
import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";

@Component({
  selector: "app-reactive-favorite-color",
  template: `
    Favorite Color: <input type="text" [formControl]="favoriteColorControl" />
  `,
})
export class FavoriteColorComponent {
  favoriteColorControl = new FormControl("");
}
// 这个是一个相对复杂一点的ReactForm 的例子
import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";

@Component({
  selector: "example-app",
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div *ngIf="first.invalid">Name is too short.</div>
      <input formControlName="first" placeholder="First name" />
      <input formControlName="last" placeholder="Last name" />
      <button type="submit">Submit</button>
    </form>
    <button (click)="setValue()">Set preset value</button>
  `,
})
export class SimpleFormGroup {
  form = new FormGroup({
    first: new FormControl("Nancy", Validators.minLength(2)),
    last: new FormControl("Drew"),
  });

  get first(): any {
    return this.form.get("first");
  }

  onSubmit(): void {
    console.log(this.form.value); // {first: 'Nancy', last: 'Drew'}
  }

  setValue() {
    this.form.setValue({ first: "Carson", last: "Drew" });
  }
}
// Template Driven Form
import { Component } from "@angular/core";

@Component({
  selector: "app-template-favorite-color",
  template: `
    Favorite Color: <input type="text" [(ngModel)]="favoriteColor" />
  `,
})
export class FavoriteColorComponent {
  favoriteColor = "";
}
// 复杂一点的Template Driven Form 的例子
import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";
@Component({
  selector: "example-app",
  template: `
    <form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
      <input name="first" ngModel required #first="ngModel" />
      <input name="last" ngModel />
      <button>Submit</button>
    </form>
    <p>First name value: {{ first.value }}</p>
    <p>First name valid: {{ first.valid }}</p>
    <p>Form value: {{ f.value | json }}</p>
    <p>Form valid: {{ f.valid }}</p>
  `,
})
export class SimpleFormComp {
  onSubmit(f: NgForm) {
    console.log(f.value); // { first: '', last: '' }
    console.log(f.valid); // false
  }
}

这个例子里面遇到几个关键字[formControl]=xxx,[formGroup]=xxx,formControlName=xxx
NgForm NgModel
那么这些个关键字干了啥事,它们是怎么做到的。

  • 首先谈谈H5form功能,以及 Angular Form的功能。
    • 获取一个对象的值{obj1:val1,obj2:val2},同时能够支持对每个属性配置 validaiton.
    • Angular 在此基础上提供了一种基于 API 的操作方式,包括,值,validaiton, 以及对象的结构。所有这些操作不需要操作 DOM,只需要操作Formxxx提供的 API, API 接口,主要来自于AbstractFormControl这个接口
  • Angular 是如何做到的。这个里面两个数据流,一个是 DOM 到 我们的客户端代码,另一个是从我们的客户端代码触发到 DOM。对于 Angular 而言,它就是个搬运工,一边是 DOM,一边是客户端代码。Angular 对这两个对象做了抽象,虚化出两个类型,DOM 对应于ControlValueAccessor,客户端代码则对于与AbstractFormControl这个接口。
    • formControlDirective, 以这个为例,我们可以认为这个就是 Angular,它的内部同时拥有FormControl(客户端代码),ControlValueAccessor(DOM),那么它是怎么拿到这两个东西的呢。看源码片段.FormControl来自于Input,ControlValueAccessor来自于 DI,大部分情况下,我们用的是默认的DEFAULT_VALUE_ACCESSOR,源码如下:
// FormControl 源码片段
@Directive({
  selector: "[formControl]",
  providers: [formControlBinding],
  exportAs: "ngForm",
})
export class FormControlDirective
  extends NgControl
  implements OnChanges, OnDestroy
{
  @Input("formControl") form!: FormControl;
  constructor(
    @Optional()
    @Self()
    @Inject(NG_VALIDATORS)
    validators: (Validator | ValidatorFn)[],
    @Optional()
    @Self()
    @Inject(NG_ASYNC_VALIDATORS)
    asyncValidators: (AsyncValidator | AsyncValidatorFn)[],
    @Optional()
    @Self()
    @Inject(NG_VALUE_ACCESSOR)
    valueAccessors: ControlValueAccessor[],
    @Optional()
    @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING)
    private _ngModelWarningConfig: string | null
  ) {
    super();
    this._setValidators(validators);
    this._setAsyncValidators(asyncValidators);
    this.valueAccessor = selectValueAccessor(this, valueAccessors);
  }
}
//DEFAULT_VALUE_ACCESSOR 源码
export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DefaultValueAccessor),
  multi: true,
};
@Directive({
  selector:
    "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]",
  // TODO: vsavkin replace the above selector with the one below it once
  // https://github.com/angular/angular/issues/3011 is implemented
  // selector: '[ngModel],[formControl],[formControlName]',
  host: {
    "(input)": "$any(this)._handleInput($event.target.value)",
    "(blur)": "onTouched()",
    "(compositionstart)": "$any(this)._compositionStart()",
    "(compositionend)": "$any(this)._compositionEnd($event.target.value)",
  },
  providers: [DEFAULT_VALUE_ACCESSOR],
})
export class DefaultValueAccessor
  extends BaseControlValueAccessor
  implements ControlValueAccessor
{
  /** Whether the user is creating a composition string (IME events). */
  private _composing = false;

  constructor(
    renderer: Renderer2,
    elementRef: ElementRef,
    @Optional()
    @Inject(COMPOSITION_BUFFER_MODE)
    private _compositionMode: boolean
  ) {
    super(renderer, elementRef);
    if (this._compositionMode == null) {
      this._compositionMode = !_isAndroid();
    }
  }

  /**
   * Sets the "value" property on the input element.
   * @nodoc
   */
  writeValue(value: any): void {
    const normalizedValue = value == null ? "" : value;
    this.setProperty("value", normalizedValue);
  }

  /** @internal */
  _handleInput(value: any): void {
    if (!this._compositionMode || (this._compositionMode && !this._composing)) {
      this.onChange(value);
    }
  }

  /** @internal */
  _compositionStart(): void {
    this._composing = true;
  }

  /** @internal */
  _compositionEnd(value: any): void {
    this._composing = false;
    this._compositionMode && this.onChange(value);
  }
}
  • NgModel,这个的情况有点复杂,相同点,它也有ControlValueAccessor,AbstractFormControl,ControlValueAccessor来自于 DI, AbstractFormControl 是它自己创建的。

  • 这两种模式下的数据流的,js 到 DOM 的数据流如下:

    • 代码调用AbstractFormControl.setValue(val),
    • 由于初始化时候,Angular 以及注册了AbstractFormControl.valueChanges事件,这个时候,注册的方法会被执行,这个方法里面会调用ControlValueAccessor.writeValue(val)这样,DOM 就刷新了。
  • DOM 到 js 的数据流如下,

    • Angular 一开始会注册ControlVAlueAccessor.registerOnChange,当 DOM 值变化时,会自动调用这个方法,这个方法里面会调用AbstractFormControl.setValue(val)
    • 然后会调用 js 到 DOM 的流程,不会出现循环触发的。
  • NgModel 的方式除了上面的流程,还有一个第三点,我们经常使用的场景时[(ngModel)]=xxx,也就是说,我们的双向数据流是我们的值也就是ngModel与 DOM 之间的。

    • 当我们的值改变了,在下一次 CD,ngModel这个组件会发现差异,它会在一个微任务里面调用AbstractFormControl.setValue(),然后走上面介绍的流程。
    • 当我们的 DOM 发生了改变,也是走上面的流程,我们的FormControl会拿到更新的值,同时会调用ngModelChange.emit(val),这样我们的代码就拿到了值。

下面是来自于NgModel的源码片段。

// 注册方法,当ControlValueAccess 值变化,View 该怎么变化, dir: NgModel,
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
  dir.valueAccessor!.registerOnChange((newValue: any) => {
    control._pendingValue = newValue;
    control._pendingChange = true;
    control._pendingDirty = true;

    if (control.updateOn === 'change') updateControl(control, dir);
  });
}

// 这个是FormControl值变化的时候,ControlValueAccessor 的响应。
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
  const onChange = (newValue: any, emitModelEvent: boolean) => {
    // control -> view
    dir.valueAccessor!.writeValue(newValue);

    // control -> ngModel
    if (emitModelEvent) dir.viewToModelUpdate(newValue);
  };
  control.registerOnChange(onChange);

  // Register a callback function to cleanup onChange handler
  // from a control instance when a directive is destroyed.
  dir._registerOnDestroy(() => {
    control._unregisterOnChange(onChange);
  });
}

function updateControl(control: FormControl, dir: NgControl): void {
  if (control._pendingDirty) control.markAsDirty();
  control.setValue(control._pendingValue, {emitModelToViewChange: false});
  dir.viewToModelUpdate(control._pendingValue);
  control._pendingChange = false;
}

//**************************NgModel************************
// NgModel 内部的一个方法,主要就是发出(onModelChange)事件
 override viewToModelUpdate(newValue: any): void {
    this.viewModel = newValue;
    this.update.emit(newValue);
  }

  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;
    }
  }
  // 这时一个微任务,会在CD之后做。
  private _updateValue(value: any): void {
    resolvedPromise.then(() => {
      this.control.setValue(value, {emitViewToModelChange: false});
    });
  }

//**************************NgModel************************
posted @ 2021-10-24 20:56  kongshu  阅读(132)  评论(0编辑  收藏  举报