Recently, I made a function of dynamic form input verification, and encountered a bug that cost a lot of overtime. Finally, I found that the knowledge blind area was caused by the unclear principle of vUE responsiveness. The following is a summary of my study.

Business requirements: The following figure needs to implement input prompts accurate to each input box. Among them, the rank is not determined by the number of columns, and dynamic rendering is performed according to the grade data queried. For specific services, click the Set button, and the form will display information about the selected item (the selected item comes from the check box of the form, which is omitted below). Click the batch Set button to reset the form to the default value.

Here is part of the code:

<div v-if="formData.distributionInvestmentType=='20'">
  <div class='be-center'>
    <div class="item">Percentage of commodity commission</div>
    <div
      class="item"
      v-for="item in distributionLevels"
      :key="'customRatioGoods_'+item.distributionLevelCode"
    >
      <el-form-item
        :prop="'customRatio.'+'goods_'+item.distributionLevelCode"
        :rules="customRatioRule"
      >
        <el-input
          size='mini'
          placeholder="0-100, two decimal places at most."
          v-model="formData.customRatio['goods_'+item.distributionLevelCode]"
          @input="handleForceUpdateInput"
        ></el-input>%
      </el-form-item>
    </div>
  </div>   
</div>
// The form model defined in data:
formData: {
  extensionStatus: "10".distributionInvestmentType: "10".customRatio: {},
  customMoney: {},},// Get the level data for the form's el-input binding V-model
getLevels() {
  let data = [
    {
      distributionLevelCode: "DL2110180001".distributionLevelName: "Grade one",},/ /...
  ];
  this.distributionLevels = data;
  for (let item of data) {
    // The new attribute should use this.$set so that the new attribute is also responsive
    Direct assignment / / note 1: the new attributes, attribute not responsive, through Object. GetOwnPropertyDescriptor printing property descriptors, no corresponding get set
    this.formData.customRatio["goods_" + item.distributionLevelCode] = "";
    / /...}},handleResetForm() {
  this.formData = {
    extensionStatus: "10".dangerDays: "1".distributionInvestmentType: "10".customRatio: this.resetObj(this.formData.customRatio),
    customMoney: this.resetObj(this.formData.customMoney)
  };
  // Annotation 2: formData is assigned to a new object, and the new object is processed responsively in the setter for formData
},
Copy the code

The problem

$set = $set; $set = $set; $set = $set;

Mark 1: FormData is defined in data only to the customRatio layer, that is, reactive data only to the customRatio layer. Input box binding of v – mode = “formData. CustomRatio [‘ goods_ + item. DistributionLevelCode]” is formData customRatio under additional properties, direct assignment, a new attribute is not responsive data, This leads to the next few problems.

First problem: Input box cannot be typed. Click the “set” button, the form input box displays the data of the selected item in the table, but the input box does not update the value of the output. Check vue-devTools, the data in the data has been updated, and then bind @input event to the input box. Call this.$forceUpdate to force the update, and the input box will display the value in real time.

Second problem: Although the input box is normal, the custom verification rule does not get the latest input value, and the value is always the output value, so input verification cannot be done.

Because knowledge is limited, behind opened all sorts of demonized guess, debugging. El-form-item dynamic rules and prop values, and the definition of El-Form rules. Repeated attempts are fruitless, from itching to expressionless 😂.

To solve

Note 2: During debugging, it was found that the commission type option in the popover was switched, resulting in re-rendering of the el-form-item, which can make the rules check rule get the latest input value once, and still not update later. . Later found that the batch setting is always normal, without these problems. Comparing the data processing of setting and batch setting, it is found that batch setting assigns a value to a new object, while setting always modifies the assignment directly on the original object. Later, through consulting materials and blogs, I learned that it is related to vUE responsive processing.

defineReactive

When a new object is assigned to an observed-responsive data, all attributes of the new object are recursively defineReactive, making the new object also reactive, similar to react immutable.

SRC /core/observer/
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }

  letchildOb = ! shallow && observe(val)// Explain: Value is an object, recurse all attributes of value, again call defineReactive. Value is an array, and the __proto__ attribute of value points to arrayMethods
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // Explanation: Dependency collection
        if (childOb) {
          childOb.dep.depend() // Explanation: Nested object dependency collection
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal)// Explanation: The value of a reactive object attribute is assigned to a new object, which performs reactive processing
      dep.notify()
    }
  })
}
Copy the code

The key is in the end! Shallow && Observe (newVal) Here, observe the new object used for assignment.

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 
    ob = value.__ob__  // Explanation: Reactive data returns an existing __ob__, non-reactive data calls a new Observer
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this) // The __ob__ data is a responsive identifier
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

/** * Augment a target Object or Array by intercepting * the prototype chain using __proto__ */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src  // resets the __proto__ of the responsive array data to the arrayMethods object that vue overwrites. ArrayMethods overwrites several arrayMethods. When the corresponding arrayMethods are called to modify the array, notify is triggered
  /* eslint-enable no-proto */
}
Copy the code

$set( target, propertyName/index, value )

Scope: Add a property to a reactive object and make sure the new property is also reactive.

export function set (target: Array<any> | Object, key: any, val: any) :any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val) // Explanation: Call the splice method overridden by arrayMethods to trigger notify
    return val
  }
  if (key intarget && ! (keyin Object.prototype)) { // Description: Non-new attribute, no responsive processing, exit after assignment
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if(! ob) {// Description: Non-responsive data, no responsive processing, exit after assignment
    target[key] = val 
    return val
  }
  defineReactive(ob.value, key, val) // Explanation: New properties of reactive objects perform reactive processing
  ob.dep.notify()
  return val
}
Copy the code

After this study, FOUND that VUE is still quite advanced, before mentioned vUE response type principle, just roughly know is defineProperty data hijacking, and then deep don’t understand, tune this bug, found that there are many, continue to learn, continue to volume 🤪.

Above, welcome to discuss, if there are mistakes, welcome to correct, I hope to help you 😀