preface

Vue official interpretation of the responsive principle: in-depth responsive principle

To summarize the official description, it can be divided into the following points:

  • Component instances have their own Watcher objects for logging data dependencies
  • Each property of data in a component has its own getter and setter methods for collecting dependencies and firing dependencies
  • During component rendering, the getter method of the property in Data is called to collect the dependencies into the Watcher object
  • A property change in data calls a method in the setter to tell Watcher that a dependency has changed
  • Watcher receives the message of the dependency change, rerenders the virtual DOM, and implements the page response

However, the official introduction is only a general process, we still do not know how vue set getter and setter methods for each property of data. What is the difference between the implementation of object properties and array properties? How to implement dependency collection and dependency triggering? To find out, you’ll have to look at a wave of source code. Next, please follow me from vUE source code analysis of vUE responsive principle

— Now I’m going to start my show

Instance initialization phase

Vue source code instance/init.js is the entry to initialization, which is divided into the following steps:

// Initialize the life cycle
initLifecycle(vm)
// Initialize the event
initEvents(vm)
// Initialize render
initRender(vm)
// Trigger the beforeCreate event
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// Initialize state,!! Highlight here!!
initState(vm)
initProvide(vm) // resolve provide after data/props
// Trigger an created event
callHook(vm, 'created')
Copy the code

In the initState() method, props, methods, data, computed, and watcher are initialized. You can see the following code in instance/state.js.

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // Initialize props
  if (opts.props) initProps(vm, opts.props)
  // Initialize methods
  if (opts.methods) initMethods(vm, opts.methods)
  // Initialize data!! Highlight again!!
  if (opts.data) {
    initData(vm)
  } else {
  	// Call the observe _data object even if there is no data
    observe(vm._data = {}, true /* asRootData */)}// Initialize computed
  if (opts.computed) initComputed(vm, opts.computed)
  // Initialize watcher
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

Data is initialized in the highlighted initData() method. The code is still visible in instance/state.js. The initData() method code is as follows (abridged).

/* Initializes data */
function initData (vm: Component) {
  // Determine if data is an object
  if(! isPlainObject(data)) { ... }// Check whether attributes in data have the same name as methods
  if (methods && hasOwn(methods, key)) {
    ...
  }
  // Check whether the attribute in data has the same name as props
  if (props && hasOwn(props, key)) {
    ...
  }
  // Move the attributes in the VM to vm._data
  proxy(vm, `_data`, key)
  // Invoke the observe data object
  observe(data, true /* asRootData */)}Copy the code

The initData() function, in addition to the previous series of data judgments, is the data proxy and the observe method call. Data proxy(VM, ‘_data’, key) is used to proxy vm attributes to vm._data, for example:

// The code is as follows
const per = new VUE({
        data: {name: 'summer'.age: 18,}})Copy the code

When we access per.name, we are actually accessing per._data.name and the following observe(data, true /* asRootData */) is the beginning of the response.

summary

The initialization process is summarized as follows

Responsive stage

In observe/index.js, observe is a factory function that generates an observe instance for the object. It is the Observe instance returned by the Observe factory function that turns the object into a responsive object.

Observe constructor

The code for the Observe constructor is as follows (an abridged version).

export class Observer {
  constructor (value: any) {
  	// The object itself
    this.value = value
    // Rely on the collector
    this.dep = new Dep()
    this.vmCount = 0
    // Add an __ob__ attribute to the object
    def(value, '__ob__'.this)
    // If the object is array
    if (Array.isArray(value)) {
     	...
    } else {
      // If the object is of type Object. }}Copy the code

From code analysis, the Observe constructor does three things:

  • Add to the object__ob__Properties,__ob__Contains the value data object itself, the DEP dependent collector, and vmCount. After this step, the data changes as follows:
	/ / the original data
	const data = {
        name: 'summer'
	}
	// Data after change
	const data = {
        name: 'summer'.__ob__: {
            value: data, //data the data itself
            dep: new Dep(), // DEP depends on the collector
            vmCount: 0}}Copy the code
  • If the object type is array, perform the array operation
  • If the object type is Object, the object type operation is performed

The data is of type Object

When the data is of type Object, a walk method is called in which all attributes of the data are iterated over and the defineReactive method is called. The defineReactive method code is still in observe/index.js, with the following abridged version:

export function defineReactive (.) {
  // DeP stores dependent variables. Each property field has its own DEP, which is used to collect dependencies belonging to that field
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // Cache the original get and set methods
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }
  // Create childOb for each attribute and observe recursion for each attribute
  letchildOb = ! shallow && observe(val)// Add getter/setter methods to attributes
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {... },set: function reactiveSetter (newVal) {... })}Copy the code

The defineReactive method does a few things:

  • Instantiate a DEP dependency collector for each attribute to collect the associated dependencies for that attribute. [Reference through getter, setter]
  • Caches the original get and set methods of the property to ensure the normal behavior when rewriting get and set methods later.
  • Create childOb for each attribute. It’s a process of observing recursion of attributes and storing the results in childOb. The childOb of an object or array property is__ob__, the childOb of other attributes is undefined). [Reference through getter, setter]
  • Add getter and setter methods to each property in the object.

The data processed by defineReactive changes as follows, each property has its own DEP, childOb, getter, setter, and __ob__ for each property of type object

/ / the original data
const data = {
    user: {
        name: 'summer'
    },
    other: '123'
}
// Processed data
const data = {
    user: {
        name: 'summer',
        [name dep,]
        [name childOb: undefined]
        name getter,// Reference name dep and name childOb
        name setter,// Reference name dep and name childOb
        
        __ob__:{user, dep, vmCount}
    },
    [user dep,]
    [user childOb: user.__ob__,]
    user getter,// Reference user dep and user childOb
    user setter,// Reference user dep and user childOb
    
    other: '123',
    [other dep,]
    [other childOb: undefined,]
    other getter,// Reference other dep and other childOb
    other setter,// Reference other dep and other childOb
    
    __ob__:{data, dep, vmCount}
}
Copy the code

The last step in the defineReactive function is to add getter and setter methods to each attribute. So what do getter and setter functions do?

In the getter method:

The internal code of the getter function is as follows:

get: function reactiveGetter () {
	// Call the get method of the original property to return the value
	const value = getter ? getter.call(obj) : val
    // If there are dependencies that need to be collected
    if (Dep.target) {
        /* Collect the dependencies into the deP of the property */
        dep.depend()
        if (childOb) {
          This dependency is also collected in obj.__ob__.dep for each object
          childOb.dep.depend()
          DependArray if the attribute is array type
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
    }
	return value
},
Copy the code

The getter method does two main things:

  • Call the get method of the original property to return the value
  • Collect rely on
    1. Dep.target represents a dependency, the observer, and in most cases, a dependency function.
    2. If a dependency exists, it is collected into the DEP dependency collector that relies on that attribute
    3. If there is a childOb (that is, the property is an object or array), the dependency is collected into childOb, that is__ob__Dependency collector__ob__.depIn, the dependency collector is touched when adding a new attribute to an attribute object using $set or vue. set, that is, vue. set or vue. delete__ob__.depDependency in.
    4. If the property value is an array, the dependArray function is called to collect the dependency for each object element in the array__ob__.depIn the. Ensure that nested objects in the array respond properly when $set or vue. set is used. The code is as follows:
/ / data
const data = {
    user: [{name: 'summer'}}]// Page display
{{user}}
<Button @click="addAge()">addAge</Button>
// the addAge method adds an age attribute to a nested object in an array
addAge: function(){
	this.$set(this.user[0].'age'.18)}Copy the code
/ / dependArray function
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // Collect dependencies into each child object/array
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
Copy the code
// Converted data
const data = {
    user: [{name: 'summer'.__ob__: {user[0], dep, vmCount}
        }
        __ob__: {user, dep, vmCount}
    ]
}
Copy the code

DependArray collects user’s dependencies into its internal user[0] object __ob__.dep so that the page responds to addAge changes.

In setter methods:

The internal code of the setter function is as follows:

set: function reactiveSetter (newVal) {
      // Set the correct value for the property
      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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // Create a new childOb for the property because its value has changed, and re-observechildOb = ! shallow && observe(newVal)// Execute all dependencies in the dependency in the set method
      dep.notify()
      }
})
Copy the code

Setter methods do three main things:

  • Set the correct value for the property
  • Since the value of the property has changed, create a new childOb for the property and re-observe
  • Performs all dependencies in a dependent

Now that we’re done with the data being pure object type, let’s look at the operations where the data is array type.

The data is of array type

Observer /index.js for array:

if (Array.isArray(value)) {
	const augment = hasProto
		? protoAugment
		: copyAugment
	// Intercepts the modify array method
	augment(value, arrayMethods, arrayKeys)
	// Recursively observe each value in the array
	this.observeArray(value)
}
Copy the code

When the data type is array

  1. Specify constructors for data using the protoAugment method__protoArrayMethods for compatibility if the browser does not support it__proto__, arrayMethods is used to override all related methods in array data.
  2. Recursively observe each value in the array
ArrayMethods Intercepts methods that modify arrays

ArrayMethods is defined in observe/array.js as follows:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

// Modify the array method
const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // Intercepts methods that modify an array, triggering all dependencies in __ob__.dep in the array when the modified array method is called
  def(arrayMethods, method, function mutator (. args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // Observe the new element using observeArray
    if (inserted) ob.observeArray(inserted)
    // Triggers all dependencies in __ob__.dep
    ob.dep.notify()
    return result
  })
})

Copy the code

ArrayMethods does the following:

  • The methods that need to be intercepted to modify the array are push, POP, Shift, unshift, splice, sort, reverse
  • When a new element is added to the array, observe the new element using observeArray
  • Intercepts methods that modify an array, firing when the modify array method is called__ob__.depAll dependencies of
ObserveArray recursively observes each item in the array

ObserveArray code:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}
Copy the code

In the observeArray method, observe the observeArray recursion for all properties in the array. One problem, however, is that you cannot observe all the non-object primitive types in an array. The first sentence of the observe method is

if(! isObject(value) || valueinstanceof VNode) {
	return
}
Copy the code

That is, values in an array that are not of type Object will not be observed if there is data:

const data = {
    arr: [{
    	test: 0
   	}, 1.2],}Copy the code

Change ARr [0]. Test =3 can trigger a response, change ARR [1]=4 can not trigger a response, because observeArray observation of each item, observe(ARR [0]) one observation one object can be observed. Observe (arr[1]) observe a basic type of data and cannot be observed.

summary

Responsive phase flow chart

Reference article: Lifting the veil on data response systems