SoFunction
Updated on 2025-04-07

Use of data response system for Vue source code analysis

Next, let’s focus on Vue’s data response system. When I talk about data response, I first use a simple example to introduce the idea of ​​bidirectional data binding, and then look at the source code. This method is also borrowed from here, and it feels that this is indeed more conducive to understanding.

Ideas for bidirectional data binding

1. Object

Let’s first look at the situation where elements are objects. Suppose we have an object and a monitoring method:

const data = {
 a: 1
};
/**
 * exp[String, Function]: Observed field
 * fn[Function]: Method to execute after the observed object is changed
 */
function watch (exp, fn) {

}

We can call the watch method and print a sentence when the value of a is changed:

watch('a', () => {
 ('a changed')
})

To implement this function, we must first know that property a has been modified. At this time, you need to use a function to turn property a into accessor attribute:

(data, 'a', {
 set () {
  ('Set a')
 },
 get () {
  ('Readed a')
 }
})

In this way, when we modify the value of a: = 2, a will be printed and a will be printed. When we obtain the value of a:, a will be printed and read.

We have been able to intercept and do some operations in the reading and setting of properties. However, when modifying the attribute, we do not want to always print the sentence a set, but there is a monitoring method watch. Different attributes have different operations, and it may also be monitored multiple times for the same attribute.

This requires a container to collect the listening dependencies on the same attribute, and then take it out and trigger it in turn when the attribute changes. Since the dependency is triggered when the attribute changes, we can put it in the setter and collect the dependencies in the getter. Here we will not consider some cases such as replication of dependencies.

const dep = [];
(data, 'a', {
 set () {
  (fn => fn());
 },
 get () {
  (fn);
 }
})

We define the container dep. When reading the a property, we trigger the get function to store the dependencies into the dep; when setting the property, we trigger the set function to execute the dependencies in the container one by one.

So where does fn come from? Let's look at some of our monitoring functions watch

watch('a', () => {
 ('a changed')
})

This function has two parameters. The first is the observed field, and the second is the operation that needs to be triggered after the value of the observed field is changed. In fact, the second parameter is the dependency fn we want to collect.

const data = {
 a: 1
};

const dep = [];
(data, 'a', {
 set () {
  (fn => fn());
 },
 get () {
  // Target is the dependency function of this variable  (Target);
 }
})

let Target = null;
function watch (exp, fn) {
 // Assign fn to Target Target = fn;
 // Read attributes, trigger get function, collect dependencies data[exp];
}

Now we can only observe one attribute a. In order to observe all attributes on the object data, we will encapsulate the code that defines the accessor attributes:

function walk () {
 for (let key in data) {
  const dep = [];
  const val = data[key];
  (data, key, {
   set (newVal) {
    if (newVal === val) return;
    val = newVal;
    (fn => fn());
   },
   get () {
    // Target is the dependency function of this variable    (Target);
    return val;
   }
  })
 }
}

Use a for loop to traverse all properties on data, and change it to the accessor attribute for each property.

Now there is no problem monitoring the properties of the basic type value in data. What if the property value of data is another object:

data: {
 a: {
  aa: 1
 }
}

Let's change our walk function again. When val is still an object, call walk recursively:

function walk (data) {
 for (let key in data) {
  const dep = [];
  const val = data[key];
  // If val is an object, call walk recursively and convert its attributes into accessor attributes  if ((val) === '[object Object]') {
   walk(val);
  }

  (data, key, {
   set (newVal) {
    if (newVal === val) return;
    val = newVal;
    (fn => fn());
   },
   get () {
    // Target is the dependency function of this variable    (Target);
    return val;
   }
  })
 }
}

A piece of judgment logic was added, and if the property value of a certain property is still an object, the walk function will be called recursively.

Although the above modification is the accessor attribute, the following code still cannot run:

watch('', () => {
 ('Modified')
})

Why is this? Let's look at our watch function:

function watch (exp, fn) {
 // Assign fn to Target Target = fn;
 // Read attributes, trigger get function, collect dependencies data[exp];
}

When reading the attribute, it is data[exp], and when putting it here, it is data[], which is naturally wrong. The correct way to read should be data[a][aa]. We need to modify the watch function:

function watch (exp, fn) {
 // Assign fn to Target Target = fn;
 
 let obj = data;
 if (/\./.test(exp)) {
  const path = ('.');
  (p => obj = obj[p])

  return;
 }


 data[exp];
}

A judgment logic is added here. When the monitored field contains ., the content of the if statement block is executed. First use the split function to convert the string to an array: => [a, aa]. Then use a loop to read the nested property values ​​and return ends.

Vue provides a $watch instance method to observe expressions, and replaces complex expressions with functions:

// Functionvm.$watch(
 function () {
 // Expression ` + ` Each time a different result is obtained // The processing functions will be called. // It's like listening to an undefined computed property return  + 
 },
 function (newVal, oldVal) {
 // Do something }
)

When the first function is executed, a get interceptor will be triggered to collect dependencies.

What changes should be made to the watch function when the first parameter of our watch function is a function? To be able to collect dependencies, you have to read the attribute and trigger the get function. How to read attributes when the first parameter is a function? There are read attributes in the function, so just execute the function.

function watch (exp, fn) {
 // Assign fn to Target Target = fn;
 
 // If exp is a function, execute the function directly if (typeof exp === 'function') {
  exp()
  return
 }

 let obj = data;
 if (/\./.test(exp)) {
  const path = ('.');
  (p => obj = obj[p])

  return;
 }


 data[exp];
}

This is the end of the processing of objects. Let’s look at the details in the source code.

2. Array

There are several mutated methods in an array that change the array itself: push pop shift unshift splice sort reverse, so how can you know when these mutated methods are called? We can extend the method while ensuring that the original method functions remain unchanged. But how to expand?

The methods of array instances all come from the prototype of the array constructor. The __proto__ attribute of the array instance points to the prototype of the array constructor, that is: arr.__proto__ === . We can define an object, its prototype points to, and then redefine the function that is duplicated with the mutated method in this object, and then let the __proto__ of the instance point to the object. In this way, when calling the mutated method, the redefined method will be called first.
const mutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

// Create an object that is prototypedconst arrayMethods = ();
// cacheconst originMethods = ;

(method => {
 arrayMethods[method] = function (...args) {
  // Call the original method to get the result  const result = originMethods[method].apply(this, args);
  (`Redefined${method}method`)
  return result;
 }
})

Let's test it:

const arr = [];
arr.__proto__ = arrayMethods;
(1);

You can see that the sentence redefined push method was printed on the console.

Let’s have a rough impression first, let’s take a look at the source code.

Instance object proxy access data

In the initState method, there is a piece of code like this:

const opts = vm.$options
...
if () {
 initData(vm)
} else {
 observe(vm._data = {}, true /* asRootData */)
}

opts is vm.$options. If it exists, execute the initData method. Otherwise, execute the observe method and assign a null object to vm._data. Let’s start with the initData method and start the road to exploring data response systems.

The initData method is defined in the core/instance/ file:

function initData (vm: Component) {
 let data = vm.$
 data = vm._data = typeof data === 'function'
 ? getData(data, vm)
 : data || {}
 if (!isPlainObject(data)) {
 data = {}
 .NODE_ENV !== 'production' && warn(
  'data functions should return an object:\n' +
  '/v2/guide/#data-Must-Be-a-Function',
  vm
 )
 }
 // proxy data on instance
 const keys = (data)
 const props = vm.$
 const methods = vm.$
 let i = 
 while (i--) {
 const key = keys[i]
 if (.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)) {
  .NODE_ENV !== 'production' && warn(
  `The data property "${key}" is already declared as a prop. ` +
  `Use prop default value instead.`,
  vm
  )
 } else if (!isReserved(key)) {
  proxy(vm, `_data`, key)
 }
 }
 // observe data
 observe(data, true /* asRootData */)
}

There is a bit too much content, let's look at it from the top to the bottom. First, it's a piece of code like this:

let data = vm.$
 data = vm._data = typeof data === 'function'
 ? getData(data, vm)
 : data || {}

We know that after the option merge, data has become a function. So why is there a judgment here whether data is a function? This is because beforeCreate lifecycle is called before the initState function after the mergeOptions function, and the mergeOptions function handles the merge option merge. What if the user modified the value of vm.$ in beforeCreate? Then it may not be a function. After all, the user's operations are uncontrollable, so it is still necessary to make a judgment here.

Under normal circumstances, data is a function, and the getData function will be called and the data and Vue instance vm will be passed as parameters. This function is also defined in the current page:

export function getData (data: Function, vm: Component): any {
 // #7573 disable dep collection when invoking data getters
 pushTarget()
 try {
 return (vm, vm)
 } catch (e) {
 handleError(e, vm, `data()`)
 return {}
 } finally {
 popTarget()
 }
}

In fact, this function gets the data object by calling data and returns: (vm, vm). The wrapping with try...catch is to catch possible errors. If there is an error, call the handleError function and return an empty object.

PushTarget and popTarget are called at the beginning and end of the function, respectively, to prevent redundant dependencies when initializing data using props data.

Let’s go back to the initData function, so now data and vm._data are the final data objects.

Next is an if judgment:

if (!isPlainObject(data)) {
 data = {}
 .NODE_ENV !== 'production' && warn(
  'data functions should return an object:\n' +
  '/v2/guide/#data-Must-Be-a-Function',
  vm
 )
 }

isPlainObject determines whether it is a pure object. If data is not an object, it will give a warning message in a non-production environment.

Continue to read:

// proxy data on instance
// Get the key of the data objectconst keys = (data)
// Get props, it is an objectconst props = vm.$
// Get methods, it is an objectconst methods = vm.$
let i = 
// Loop through the keys of datawhile (i--) {
 const key = keys[i]
 // If methods exist and there is the same key as the data object in methods, issue a warning.  Data priority if (.NODE_ENV !== 'production') {
  if (methods && hasOwn(methods, key)) {
  warn(
   `Method "${key}" has already been defined as a data property.`,
   vm
  )
  }
 }
 // If props exist and there is the same key as the data object in props, issue a warning.  Props priority if (props && hasOwn(props, key)) {
  .NODE_ENV !== 'production' && warn(
  `The data property "${key}" is already declared as a prop. ` +
  `Use prop default value instead.`,
  vm
  )
 } else if (!isReserved(key)) { // The isReserved function is used to detect whether a string starts with $ or _, and is mainly used to determine whether the key name of a field is reserved.  proxy(vm, `_data`, key)
 }
}
// observe data
observe(data, true /* asRootData */)

The two if conditions in while determine whether there are the same keys as the data object in props and methods, because the properties in these three can be accessed through the instance object proxy, and if the same, conflicts will occur.

const vm = new Vue({
 props: { a: { default: 2 } }
 data: { a: 1 },
 methods: {
  a () {
   (3)
  }
 }
})

When called, the overwrite phenomenon will occur. To prevent this from happening, we made a judgment here.

Let’s look at the content in else if. When !isReserved(key) is established, execute proxy(vm,_data, key). The function of the isReserved function is to determine whether a string starts with $ or _, because variables inside Vue start with $ or _ to prevent conflicts. If the key does not start with $ or _, the proxy function will be executed

const sharedPropertyDefinition = {
 enumerable: true,
 configurable: true,
 get: noop,
 set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  = function proxyGetter () {
 return this[sourceKey][key]
 }
  = function proxySetter (val) {
 this[sourceKey][key] = val
 }
 (target, key, sharedPropertyDefinition)
}

The proxy function defines the same accessor attribute as the data data field on the instance object vm, and the proxy value is the corresponding attribute value on vm._data. When accessed, the actual access is the value of this._data.a.

The last code is

// observe data
observe(data, true /* asRootData */)

Call observe to convert the data data object into responsive.

observe factory function

The observe function is defined in the core/observer/ file. We find the definition of this function, let's take a look at it a little bit.

if (!isObject(value) || value instanceof VNode) {
 return
}

First, determine if the data is not an object or a VNode instance, return it directly.

let ob: Observer | void

Then the ob variable is defined, which is an Observer instance. You can see that the observe function returns ob.

Here is an if...else branch:

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
 ob = value.__ob__
 } else if (
 shouldObserve &&
 !isServerRendering() &&
 ((value) || isPlainObject(value)) &&
 (value) &&
 !value._isVue
 ) {
 ob = new Observer(value)
 }

First, it is the if branch. Use hasOwn to determine whether the data object contains the __ob__ attribute, and determine whether the attribute value is an instance of Observer. If the condition is true, assign the value of value.__ob__ to ob.

Why did this judgment be made? After each data object is observed, a __ob__ attribute will be defined on the object, so this judgment is to prevent repeated observation of an object.

Next is the else if branch. There are a lot of judgments on this condition, let’s take a look at it one by one.

ShouldObserve must be true

This variable is also defined in the core/observer/ file,

/**
 * In some cases we may want to disable observation inside a component's
 * update computation.
 */
export let shouldObserve: boolean = true

export function toggleObserving (value: boolean) {
 shouldObserve = value
}

This code defines the shouldObserve variable and is initialized to true. Then the toggleObserving function is defined, which receives a parameter, which is used to update the value of shouldObserve. Observe can be observed when it is true, and no observation will be performed when it is false.

!isServerRendering() must be true

The isServerRendering function is used to determine whether it is rendered on the server side. It will only be observed when it is not rendered on the server side.
((value) || isPlainObject(value)) must be true

Observation is only performed when the data object is an array or a pure object

(value) must be true

The observed data object must be extensible, and the normal object is extensible by default. The following three methods can make an object non-scalable:

()、 ()、()

!value._isVue must be true

Vue instances contain _isVue attribute, this judgment is to prevent Vue instances from being observed

After the above conditions are met, the code ob = new Observer(value) will be executed to create an Observer instance

Observer constructor

Observer is also defined in the core/observer/ file. It is a constructor used to convert data objects into responsive ones.

export class Observer {
 value: any;
 dep: Dep;
 vmCount: number; // number of vms that have this object as root $data

 constructor (value: any) {
  = value
  = new Dep()
  = 0
 def(value, '__ob__', this)
 if ((value)) {
  if (hasProto) {
  protoAugment(value, arrayMethods)
  } else {
  copyAugment(value, arrayMethods, arrayKeys)
  }
  (value)
 } else {
  (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 = (obj)
 for (let i = 0; i < ; i++) {
  defineReactive(obj, keys[i])
 }
 }

 /**
 * Observe a list of Array items.
 */
 observeArray (items: Array<any>) {
 for (let i = 0, l = ; i < l; i++) {
  observe(items[i])
 }
 }
}

The above is all the code of Observer. Now let’s start with the constructor and see what instantiated Observer does.

__ob__ attribute

The constructor started to initialize several instance properties

 = value
 = new Dep()
 = 0
def(value, '__ob__', this)

value is the parameter passed when instantiating Observer, and now it is assigned to the value attribute of the instance object. The dep attribute points to the instantiated Dep instance object, which is used to collect dependencies. The vmCount property is initialized to 0.

Then use the def function to add the __ob__ attribute to the data object, and its value is the current Observer instance object. def is defined in the core/util/ file and is the right encapsulation.

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
 (obj, key, {
 value: val,
 enumerable: !!enumerable,
 writable: true,
 configurable: true
 })
}

Use def to define the __ob__ attribute to be defined as non-enumerable, so that the object will not be traversed to it.

Assume that our data object is

data = {
 a: 1
}

Add the __ob__ attribute to become

data = {
 a: 1,
 __ob__: {
  value: data, // data data object itself  dep: new Dep(), // Dep instance  vmCount: 0
 }
}

Process pure objects

Next is an if...else judgment to distinguish arrays and objects, because the processing of arrays and objects is different.

if ((value)) {
 if (hasProto) {
  protoAugment(value, arrayMethods)
 } else {
  copyAugment(value, arrayMethods, arrayKeys)
 }
 (value)
} else {
  (value)
}

Let's first look at the situation where it is an object, that is, execution (value)

The walk function is defined below the constructor

walk (obj: Object) {
 const keys = (obj)
 for (let i = 0; i < ; i++) {
  defineReactive(obj, keys[i])
 }
}

This method uses a for loop to traverse the object's properties and calls the defineReactive method for each property.

defineReactive function

defineReactive is also defined in the core/observer/ file, find its definition:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter?: ?Function,
 shallow?: boolean
) {
 const dep = new Dep()

 const property = (obj, key)
 if (property &&  === false) {
 return
 }

 // cater for pre-defined getter/setters
 const getter = property && 
 const setter = property && 
 if ((!getter || setter) &&  === 2) {
 val = obj[key]
 }

 let childOb = !shallow && observe(val)
 (obj, key, {
 enumerable: true,
 configurable: true,
 get: function reactiveGetter () {
  ...
 },
 set: function reactiveSetter (newVal) {
  ...
 }
 })
}

Because the code is too long, some content is omitted. Let's look at it in detail later. The main function of this function is to convert the data attributes of the data object into the accessor attributes

The dep constant is first defined in the function body, and its value is a Dep instance, which is used to collect the dependencies of the corresponding fields.

Next is a piece of code:

const property = (obj, key)
if (property &&  === false) {
 return
}

First, the field attribute description object is obtained, and then determine whether the field is configurable. If it is not configurable, it will be returned directly. Because unconfigurable attributes cannot be defined by changing their attributes.

Let's continue to look:

// cater for pre-defined getter/setters
const getter = property && 
const setter = property && 
if ((!getter || setter) &&  === 2) {
 val = obj[key]
}

First save the get and set methods in the property description object. If this property is already an accessor attribute, then it has a get or set method. The following operations will use the rewrite get and set methods. In order not to affect the original read and write operations, setter/getter will be cached first.

Next is an if judgment. If the condition is met, the value of the attribute will be read.

Here is the code:

let childOb = !shallow && observe(val)

Because the property value val may also be an object, the observe is called to continue the observation. But there is a condition before that deep observation will be performed only when shallow is false. shallow is the fifth parameter of defineReactive. We did not pass this parameter when calling the function in the walk, so its value here is undefined. !shallow is true, so in-depth observations will be performed here.

We have seen it in the initRender function without conducting depth observations:

defineReactive(vm, '$attrs', parentData &&  || emptyObject, () => {
 !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
 !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)

Defining attributes $attrs and $listeners on Vue instances is non-deep observation.

Collect dependencies in get

Next is to use the settings accessor properties, and first look at the get function:

get: function reactiveGetter () {
 const value = getter ? (obj) : val
 if () {
  ()
  if (childOb) {
   ()
   if ((value)) {
   dependArray(value)
   }
  }
 }
 return value
},

The get function first needs to return the attribute value, and it also collects dependencies here.

The first line of code is to get the attribute value. First, we will determine whether the getter exists. The getter is the original get function of the property. If it exists, call the function to obtain the property value. Otherwise, val will be used as the property value.

Next is the code that collects dependencies:

if () {
 ()
 if (childOb) {
  ()
  if ((value)) {
   dependArray(value)
  }
 }
}

First, to determine whether it exists is the dependency to be collected. If it exists, execute the code in the if statement block.

The depend method execution of the () dep object is to collect dependencies.

Then determine whether childOb exists, and if it exists, execute(). So who is the value of childOb?

If we have a data object:

data = {
 a: {
  b: 1
 }
}

After observe observation, add the __ob__ attribute and becomes the following:

data = {
 a: {
  b: 1,
  __ob__: { value, dep, vmCount }
 },
 __ob__: { value, dep, vmCount }
}

For property a, childOb === .__ob__, so () is .__ob__.()

Another if judgment in the if statement:

if ((value)) {
 dependArray(value)
}

If the attribute value is an array, call the dependArray function to trigger the dependency collection of array elements one by one.

Trigger dependency in set function

set: function reactiveSetter (newVal) {
  const value = getter ? (obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
  return
  }
  /* eslint-enable no-self-compare */
  if (.NODE_ENV !== 'production' && customSetter) {
  customSetter()
  }
  // #7981: for accessor properties without setter
  if (getter && !setter) return
  if (setter) {
  (obj, newVal)
  } else {
  val = newVal
  }
  childOb = !shallow && observe(newVal)
  ()
}

The set function mainly sets attribute values ​​and triggers dependencies.

const value = getter ? (obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
 return
}

First of all, it also obtains the original attribute value. Why is this step? Because you need to compare with the new value, if the new and old values ​​are equal, you can directly return without the next operation. In the if condition, we all understand the newVal === value. What does the following condition mean?

This is because of a special value NaN

NaN === NaN // false

If newVal !== newVal, it means that the new value is NaN; if value !== value, then the old value is also NaN. Then the old and new values ​​are equal and do not need to be processed.

/* eslint-enable no-self-compare */
if (.NODE_ENV !== 'production' && customSetter) {
 customSetter()
}

In a non-production environment, if the customSetter function exists, the function will be executed. customSetter is the fourth parameter of defineReactive. As shown above, this parameter was passed when we saw initRender:

defineReactive(vm, '$attrs', parentData &&  || emptyObject, () => {
  !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)

The fourth parameter is an arrow function. When modifying vm.$attrs, the warning message $attrs is read-only. So the function of customSetter is to print auxiliary information.

if (getter && !setter) return
if (setter) {
 (obj, newVal)
} else {
 val = newVal
}

If there is a getter but no setter, return it directly. getter and setter are the get and set functions of the property itself.

The following is to set the attribute value. If the setter exists, call the setter function to ensure that the original property setting operation remains unchanged. Otherwise replace the old value with the new value.
Finally, these two codes:

childOb = !shallow && observe(newVal)
()

If the new value is also an array or pure object, the new value is unobserved. Therefore, when deep observation is required, observe must be called to observe the new value. Finally, call() to trigger the dependency.

Processing arrays

After reading the processing of pure objects, let’s take a look at how arrays are converted into responsive. Some methods of arrays change the array itself, which we call mutation methods. These methods include: push pop shift unshift reverse sort splice. How to trigger dependencies when these methods are called? Take a look at Vue's handling.

if (hasProto) {
 protoAugment(value, arrayMethods)
} else {
 copyAugment(value, arrayMethods, arrayKeys)
}
(value)

First of all, there is an if...else judgment, hasProto is defined in the core/util/ file.

// can we use __proto__?
export const hasProto = '__proto__' in {}

Determine whether the object's __proto__ property can be used in IE11 and later.

If the condition is true, call the protoAugment method, passing two parameters, one is the array instance itself, and the other is arrayMethods (agent prototype).

/**
 * 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
 /* eslint-enable no-proto */
}

The purpose of this method is to point the prototype of the array instance to the proxy prototype. In this way, when the array instance calls the mutated method, the proxy prototype redefinition method can be first used. Let's take a look at the implementation of arrayMethods, which is defined in the core/observer/ file:

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

const arrayProto = 
export const arrayMethods = (arrayProto)

const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
(function (method) {
 // cache original method
 const original = arrayProto[method]
 def(arrayMethods, method, function mutator (...args) {
 const result = (this, args)
 const ob = this.__ob__
 let inserted
 switch (method) {
  case 'push':
  case 'unshift':
  inserted = args
  break
  case 'splice':
  inserted = (2)
  break
 }
 if (inserted) (inserted)
 // notify change
 ()
 return result
 })
})

This is the entire content of this file. The file only does one thing, which is to export the arrayMethods object.

const arrayProto = 
export const arrayMethods = (arrayProto)

arrayMethods is an object created based on the prototype of the array.

const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
]

This is a mutated method that defines an array.

Then for loop through the mutation method, using def to define a method with the same name as the mutation method on the proxy prototype.

(function (method) {
 // cache original method
 const original = arrayProto[method]
 def(arrayMethods, method, function mutator (...args) {
 const result = (this, args)
 const ob = this.__ob__
 let inserted
 switch (method) {
  case 'push':
  case 'unshift':
  inserted = args
  break
  case 'splice':
  inserted = (2)
  break
 }
 if (inserted) (inserted)
 // notify change
 ()
 return result
 })
})

First, the original mutation method of the array is cached

const original = arrayProto[method]

Then use def to define a function with the same name as the mutated method on the arrayMethods object. The original function is called in the function to obtain the result

const result = (this, args)

and return result at the end of the function. It ensures that the function of the intercept function is consistent with the function of the original method.

const ob = this.__ob__
...
()

These two sentences of code trigger dependencies. When the mutated method is called, the array itself is changed, so the dependency needs to be triggered.

Let's look at the rest of the code:

let inserted
switch (method) {
 case 'push':
 case 'unshift':
  inserted = args
  break
 case 'splice':
  inserted = (2)
  break
}
if (inserted) (inserted)

The purpose of this code is to collect newly added elements and turn them into responsive data.

The parameters of push and unshift methods are the elements to be added, so inserted = args. The splice method is a new element to be added from the third parameter to the last parameter, so inserted = (2). Finally, if there is a newly added element, call the observeArray function to observe it.

The above is when the __proto__ attribute is supported, what about when it is not supported? The copyAugment method is called and three parameters are passed. The first two are the same as the parameters of the protoAugment method. One is the array instance itself, the other is the arrayMethods proxy prototype, and the other is arrayKeys.

const arrayKeys = (arrayMethods)

Its value is the name of all keys defined on the arrayMethods object, that is, the name of the mutated method to be intercepted. The function definition is as follows:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
 for (let i = 0, l = ; i < l; i++) {
 const key = keys[i]
 def(target, key, src[key]) 
 }
}

The function of this method is to define a function with the same name as the mutated method on the array instance, thereby realizing interception.

After the else code, the observeArray method (value) is called and the array instance is taken as a parameter.

The observeArray method is defined as follows:

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
 for (let i = 0, l = ; i < l; i++) {
  observe(items[i])
 }
}

Loop through the array instance and observe each item in the array again. This is because if the array element is an array or a pure object, the array element is not responsive without performing this step, and this is to achieve depth observation. for example:

const vm = new Vue({
 data: {
  a: [[1,2]]
 }
})
(1); // Can trigger response[1].push(1); // Cannot trigger the response

So you need to recursively observe array elements.

Implementation of ($set) and ($delete)

We know that adding or removing elements for objects or arrays directly cannot be intercepted. We need to use and solve it. Vue also defines $set $delete on the instance object for us to use. In fact, whether it is an instance method or a global method, their directions are the same. Let's look at their definitions below.

$set $delete is defined in the stateMixin method in the core/instance/ file

export function stateMixin (Vue: Class<Component>) {
 ...

 .$set = set
 .$delete = del

 ...
}

and define in the initGlobalAPI function in the core/global-api/ file:

export function initGlobalAPI (Vue: GlobalAPI) {
 ...

  = set
  = del

 ...
}

You can see that their function values ​​are the same. set and del are defined in the core/observer/ file. Let's first look at the definition of set

set

From top to bottom, look at the function body of set, and display this if judgment:

if (.NODE_ENV !== 'production' &&
 (isUndef(target) || isPrimitive(target))
) {
 warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}

isUndef

export function isUndef (v: any): boolean %checks {
 return v === undefined || v === null
}

Determines whether the variable is undefined or the value is null.

isPrimitive

export function isPrimitive (value: any): boolean %checks {
 return (
 typeof value === 'string' ||
 typeof value === 'number' ||
 // $flow-disable-line
 typeof value === 'symbol' ||
 typeof value === 'boolean'
 )
}

Determine whether the variable is a primitive type.

So the function of this if statement is that if target is undefined or null or its type is a primitive type, print warning information in a non-production environment.
Let's look at the next if statement:

if ((target) && isValidArrayIndex(key)) {
  = (, key)
 (key, 1, val)
 return val
}

isValidArrayIndex

export function isValidArrayIndex (val: any): boolean {
 const n = parseFloat(String(val))
 return n >= 0 && (n) === n && isFinite(val)
}

Determine whether the variable is a valid array index.

If target is an array and key is a valid array index, execute the code inside the if statement block

We know that the splice mutation method can trigger the response, (key, 1, val) takes advantage of the ability to replace the element and replace the value of the specified position element with a new value. So the array uses splice to add elements. In addition, when the index of the element to be set is greater than the array length, splice is invalid, so the length of the target takes the maximum value of the two.

if (key in target && !(key in )) {
 target[key] = val
 return val
}

The if condition means that the property has been defined on the target object, so just reset its value. Because in pure objects, the existing properties are responsive.

const ob = (target: any).__ob__
if (target._isVue || (ob && )) {
 .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
}

target._isVue
Having the _isVue attribute means this is a Vue instance'

(ob && )
ob is target.__ob__, that is target.__ob__.vmCount. Let's take a look at this code:

export function observe (value: any, asRootData: ?boolean): Observer | void {
 if (asRootData && ob) {
  ++
 }
}

asRootData indicates whether it is a root data object. What is a root data object? Let's see where the second parameter is passed when calling the observe function:

function initData (vm: Component) {
 ...

 // observe data
 observe(data, true /* asRootData */)
}

When calling observe in initData, the second parameter is true, and the data object is data. That is to say, when using the /$set function to add attributes to the root data object, it is not allowed.

Therefore, when the target is a Vue instance or a root data object, a warning message will be printed in a non-production environment.

if (!ob) {
  target[key] = val
  return val
}

When !ob is true, it means that there is no __ob__ attribute, and then target is not responsive, just change the attribute value directly.

defineReactive(, key, val)
()

Here is to add new attributes to the object and ensure that the newly added attributes are responsive.

() Trigger the response.

del

After reading the set, let’s take a look at the delete operation.

if (.NODE_ENV !== 'production' &&
  (isUndef(target) || isPrimitive(target))
 ) {
  warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}

This if judgment is the same as the set function. If the target is undefined, null, or primitive type value, print the warning message in a non-production environment.

if ((target) && isValidArrayIndex(key)) {
  (key, 1)
  return
}

When the target is an array type and the key is a valid array index value, the deletion operation is also used using splice because the mutation method can trigger the interception operation.

const ob = (target: any).__ob__
if (target._isVue || (ob && )) {
  .NODE_ENV !== 'production' && warn(
   'Avoid deleting properties on a Vue instance or its root $data ' +
   '- just set it to null.'
  )
  return
}

The same is true for this if judgment. If the target is a Vue instance or a root data object, print warning information in a non-production environment. That is, the properties of the Vue instance object cannot be deleted, nor the properties of the root data object cannot be deleted, because data itself is not responsive.

if (!hasOwn(target, key)) {
  return
}

If there is no key attribute on the target object, return it directly.

delete target[key]

After this, it means that target is a pure object and has a key attribute, so delete the attribute directly.

if (!ob) {
  return
}

If the ob object does not exist, it means that the target is not responsive and returns directly.

()

If the ob object exists, it means that the target is responsive and triggers the response.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.