current position:Home>Vue bidirectional binding principle

Vue bidirectional binding principle

2022-04-29 04:28:42weixin_ forty-two million two hundred and fifty-four thousand a

vue Two way binding principle


vue The most unique feature is its responsive system , When modifying data , The view updates accordingly
When one js Object to vue Instance as data Options ,vue Will traverse all of the objects property, And use Object.defindProperty Put these property Convert to getter/setter
these getter/setter Invisible to users , But internally, they can make vue Track dependence , Notify changes when properties are accessed and modified
Each component instance corresponds to a watcher example , It will render the exposed data in the process of component rendering property Record as dependency , When the setter When triggered , Will inform watcher, So that its associated components are re rendered

 Official flow chart

When interpreting the source code, first look at some concepts :
data:vue Data items in the instance
observer: The observer of the data , Read and write operation of monitoring data
dep: Message subscriber , Have a collection of subscribers 、 Release updated features
watcher: Subscribers to the message , You can subscribe to dep, Then accept dep Publish the update and perform the update of the corresponding view or expression
dep and watcher The relationship between :dep It's a newspaper ,watcher It's the person who subscribes to the newspaper , Subscriptions establish a relationship between them , So whenever the data is updated , The corresponding subscribers will be notified
Collection dependency :watcher Add... To your own properties dep act
Collect subscribers :dep Add... To your own properties watcher act
The following figure is based on the source code , Some of my own understanding
 Insert picture description here
Source version 2.6.11
First find vue The method of data processing initData:

//  Source directory :src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //<1>data Property agent 
      proxy(vm, `_data`, key)
    }
  }
  //<2>observe Data binding 
  // observe data
  observe(data, true /* asRootData */)
}

<1> General data The above properties are proxied to vm For instance , After that, you can go directly through vm.key visit data.key.

//  Our data objects 
var data = { a: 1 }

//  The object is added to a  Vue  In the example 
var vm = new Vue({
  data: data
})

//  Get... On this instance  property
//  Returns the corresponding field in the source data 
vm.a == data.a // => true

If the official document is correct vue Explanation of examples , A data object is added vue After instance ,vm.a == data.a //true also vm ==this //true
Besides , Only when the instance is created already exists in data Medium property It's responsive . That is, if you add a new property, Such as vm.b = 1, that b Changes to the will not trigger updates . The solution is to set the initial value .
<2> Start data binding at ,asRootData, Here is the root data , Next, we will recursively bind deep objects

//  Source directory :src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  // Check whether the current data is observe too 
    ob = value.__ob__
  } else if (  // Check whether the current data is an ordinary object , Not functions or Regexp , etc. 
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

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)  // to value increase _ob_ attribute , As data has been observer A sign of observation 
    if (Array.isArray(value)) {  //value Is an array 
      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.
   *  Traversing objects , And call the corresponding function defineReactive Add... To the attribute getter and setter
   */
  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.
   *  Traversal array , And call the corresponding function observe to item add to getter and setter
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {   
      observe(items[i])
    }
  }
}

observer There are three classes of methods ,value The value passed in is ,dep: Dep example , Used to collect subscriptions and publish updates ,vmCount: The number of times the instance was called .walk: Operation methods on objects ,observeArray: Operation method of array
stay observer Class , First of all value increase _ob_ attribute , As data has been observer A sign of observation
as follows
 Insert picture description here
You can see that the data has _ob_ sign , Indicates that it has been observed . Next , There are different operation methods for objects and arrays , For the object , Would call walk Method to traverse the properties of the object defineReactive Method binding . For arrays , Is to rewrite its seven methods
Here are defineReactive Methods specific analysis

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //<1>/* Define a in a closure dep object */
  const dep = new Dep()
  // Returns the property descriptor corresponding to a self owned property on the specified object 
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // If you have defined getter and setter function , Take it out 
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) { 
    val = obj[key]
  }

  //<2> Object's children recursively observe, And return the Observe object 
  let childOb = !shallow && observe(val)
  //<3>
  Object.defineProperty(obj, key, {
    enumerable: true,   // enumeration 
    configurable: true, // Modifiable and removable attributes 
    get: function reactiveGetter () {
      // If there were getter Method is executed 
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {  //<4>
        //  Do dependency collection 
        dep.depend()
        if (childOb) {
          // Dependency collection for child objects 
          childOb.dep.depend()
          if (Array.isArray(value)) {
            //  If it is an array, you need to collect dependencies on each member , If the array member is still an array, recursion 
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // adopt getter Method to get the current value , Compare with the new value , If it is consistent, the following operations will not be carried out 
      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)
      //dep The object informs all observers 
      dep.notify()
    }
  })
}

In the code <1> It's about ,new One. dep example ;<2> If there are nested objects at , Then make a recursive call observe, This will ensure that each layer is observe
Code <3> Place is defineReactive The core content , adopt Object.defineProperty() Of get and set Method to perform corresponding operations on the attribute
about Object.defineProperty() Of get and set Method
get:
Attribute getter function , without getter, Then for undefined. When accessing this property , This function will be called . No parameters are passed in during execution , But it will pass in this object ( By inheritance , there this It does not have to be the object that defines the property ). The return value of this function will be used as the value of the property .
The default is undefined.
set:
Attribute setter function , without setter, Then for undefined. When the property value is modified , This function will be called . This method takes a parameter ( That is, the new value given ), The this object .
The default is undefined.
When a property is accessed , You will enter get Method , stay get in , The core content is to carry out the current watcher( That is to say Dap.target) and dep Binding between dep.depend(), If the current data has child objects , Then the child objects are also bound ; If it is an array, you need to collect dependencies on each member , If the array member is still an array, recursion
When a property is modified , You will enter set Method , stay set in , The core content is through dep.notify() To inform the current dep The subscriber data bound is updated
Array processing
stay Observer Class , There is such a treatment for arrays

if (Array.isArray(value)) {  //value Is an array 
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    }

If it's an array , Replace the original method in the prototype of the array with the modified array method that can intercept the response , To achieve the effect of listening to the response of array data changes .
If the current browser supports __proto__ attribute , Directly overwrite the native array method on the current array object prototype , If this property is not supported , The prototype of the array object is directly overwritten .
arrayMethods Is an override of the array method , Specific source code :

import { def } from '../util/index'

/* Get the prototype of the native array */
const arrayProto = Array.prototype
/* Create a new array object , Seven methods to modify the array on this object , Methods to prevent contamination of native arrays */
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
 /* These array methods are overridden here , These methods of rewriting arrays without polluting the native array prototype , Intercepts changes in the members of the array , While performing native array operations dep Notify all associated observers for responsive processing */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /* Cache the native methods of the array , Call... Later */
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /* Call the native array method */
    const result = original.apply(this, args)

    /* The newly inserted element of the array needs to be redone observe To be responsive */
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
      
    // notify change
    /*dep Notify all registered observers for responsive processing */
    ob.dep.notify()
    return result
  })
})

You can see , about push,unshift,splice These three methods , need observe, Changes to other methods are updated on the current index , So there's no need to execute ob.observeArray

copyright notice
author[weixin_ forty-two million two hundred and fifty-four thousand a],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/117/202204270551122287.html

Random recommended