angular 更新表单值的两种方法: setvalue,patchvalue

使用 patchValue() 方法会比使用 setValue() 方法更好!

1、patchValue()

// angular2/packages/forms/src/model.ts
export class FormGroup extends AbstractControl {
   ...
   patchValue(
     value: {[key: string]: any},{onlySelf, emitEvent}: 
              {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
      Object.keys(value).forEach(name => {
        if (this.controls[name]) {
          this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
        }
      });
      this.updateValueAndValidity({onlySelf, emitEvent});
   }
}

// 使用示例
const form = new FormGroup({
   first: new FormControl(),
   last: new FormControl()
});

console.log(form.value);   // {first: null, last: null}

form.patchValue({first: 'Nancy'});
console.log(form.value);   // {first: 'Nancy', last: null}

从源码中我们可以看出,patchValue() 方法会获取输入参数对象的所有 key 值,然后循环调用内部控件的 patchValue() 方法,具体代码如下:

Object.keys(value).forEach(name => {
  if (this.controls[name]) {
     this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
  }
});

首先,Object.keys() 会返回对象 key 值的数组,例如:

const man = {name : 'Semlinker', age: 30};
Object.keys(man); // ['name', 'age']此外 this.controls 包含了 FormGroup 对象中的所有 FormControl 控件,我们可以通过 this.controls[name] 方式,访问到 name 对应的控件对象。

现在让我们来回顾一下创建 FormGroup 对象的相关代码

this.form = this.fb.group({
  name: ['', Validators.required],
  event: this.fb.group({
    title: ['', Validators.required],
    location: ['', Validators.required]
  })
});

与之相对应的对象模型如下:

{
  name: '',
  event: {
    title: '',
    location: ''
  }
}

因此要更新该模型的值,我们可以利用 FormGroup 对象的 patchValue() 方法:

this.form.patchValue({
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

以上代码将会通过循环的方式,更新每个 FormControl 控件。接下来我们看一下 FormControl 中 patchValue() 方法的具体实现:

patchValue(value: any, options: {
    onlySelf?: boolean,
    emitEvent?: boolean,
    emitModelToViewChange?: boolean,
    emitViewToModelChange?: boolean
  } = {}): void {
    this.setValue(value, options);
}

忽略所有的函数参数和类型,它所做的就是调用 setValue() 方法,设置控件的值。另外使用 patchValue() 方法有什么好处呢?假设我们使用 firebase,那么当我们从 API 接口获取数据对象时,对象内部可能会包含 $exists$key 属性。而当我们直接使用返回的数据对象作为参数,直接调用 patchValue() 方法时,不会抛出任何异常:

this.form.patchValue({
  $exists: function () {},
  $key: '-KWihhw-f1kw-ULPG1ei',
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

其实没有抛出异常的原因,是因为在 patchValue() 内部循环时,我们有使用 if 语句进行条件判断。

2、setValue

FormGroup 类中的 setValue() 方法的具体实现:

setValue(
  value: {[key: string]: any},
  {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
      this._checkAllValuesPresent(value);
      Object.keys(value).forEach(name => {
        this._throwIfControlMissing(name);
        this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
      });
      this.updateValueAndValidity({onlySelf, emitEvent});
}

// 使用示例
const form = new FormGroup({
    first: new FormControl(),
    last: new FormControl()
});
console.log(form.value);   // {first: null, last: null}

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

跟 patchValue() 方法一样,我们内部也是包含一个 Object.keys() 的循环,但在循环开始之前,我们会先调用 _checkAllValuesPresent() 方法,对输入值进行校验。 另外 _checkAllValuesPresent() 方法的具体实现如下:

_checkAllValuesPresent(value: any): void {
    this._forEachChild((control: AbstractControl, name: string) => {
      if (value[name] === undefined) {
        throw new Error(`Must supply a value for form control with name: '${name}'.`);
      }
    });
}

该方法内部通过 _forEachChild() 遍历内部的 FormControl 控件,来确保我们在调用 setValue() 方法时,设置的参数对象中,会包含所有控件的配置信息。如果 name 对应的配置信息不存在,则会抛出异常。

_checkAllValuesPresent() 验证通过后,Angular 会进入 Object.keys() 循环,此外在调用 setValue() 方法前,还会优先调用 _throwIfControlMissing() 判断控件是否存在,该方法的实现如下:

_throwIfControlMissing(name: string): void {
    if (!Object.keys(this.controls).length) {
      throw new Error(`
        There are no form controls registered with this group yet.  
        If you're using ngModel,
        you may want to check next tick (e.g. use setTimeout).
      `);
    }
    if (!this.controls[name]) {
      throw new Error(`Cannot find form control with name: ${name}.`);
    }
}

上面代码首先判断 this.controls 是否存在,如果存在进一步判断 name 对应的 FormControl 控件是否存在。当 _throwIfControlMissing() 验证通过后,才会最终调用 FormControl 控件的 setValue() 方法:

this.controls[name].setValue(value[name], {onlySelf: true, emitEvent})

我们来看一下 FormControl 类中,setValue() 方法的具体实现:

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});
}

该方法的第一个参数,就是我们要设置的值,第二个参数是一个对象:

  • onlySelf:若该值为 true,当控件的值发生变化后,只会影响当前控件的验证状态,而不会影响到它的父组件。默认值是 false。
  • emitEvent:若该值为 true,当控件的值发生变化后,将会触发 valueChanges 事件。默认值是 true
  • emitModelToViewChange:若该值为 true,当控件的值发生变化时,将会把新值通过 onChange 事件通知视图层。若未指定 emitModelToViewChange 的值,这是默认的行为。
  • emitViewToModelChange:若该值为 true,ngModelChange 事件将会被触发,用于更新模型。若未指定 emitViewToModelChange 的值,这是默认的行为。

其实仅仅通过上面的代码,我们还是没完全搞清楚 setValue() 方法内部真正执行流程。如我们不知道如何注册 changeFn 函数和 updateValueAndValidity() 方法的内部处理逻辑,接下来我们先来看一下如何注册 changeFn 函数

export class FormControl extends AbstractControl {
  /** @internal */
  _onChange: Function[] = [];
 ...
 /**
  * Register a listener for change events.
  */
 registerOnChange(fn: Function): void { this._onChange.push(fn); }
}

现在我们来回顾一下 setValue() 的相关知识点。对于 FormGroup 对象,我们可以通过 setValue() 方法更新表单的值,具体使用示例如下:

this.form.setValue({
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

以上代码成功运行后,我们就能成功更新表单的值。但如果我们使用下面的方式,就会抛出异常:

this.form.setValue({
  $exists: function () {},
  $key: '-KWihhw-f1kw-ULPG1ei',
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});
总结:patchValue 可以只更新副本的数据,而setValue则必须与form 数据结构一致才能进行更新。