Briefly understand the responsiveness principle of Vue2

Students who use Vue as a front-end development technology stack will have some curiosity when using Vue: Why do our responsive variables need to be defined in data? How does Vue monitor changes and implement responsiveness? This time we will explore the responsiveness principle of Vue2.

Object responsiveness

Modify attribute description to implement responsiveness

First, let’s implement basic responsiveness, that is, monitor changes in data data. More detailed comments are provided in my code.

// Determine whether it belongs to object
function isObject(obj) {<!-- -->
  // Note that the typeof of null is also 'object'
  return typeof obj === "object" & amp; & amp; obj != null;
}

// Provide responsive logic for input parameters
export function observer(obj) {<!-- -->
  // If it is not an object, return it as is
  if (!isObject(obj)) return obj;
  // Traverse the object and add responsiveness to each property
  for (const key in obj) {<!-- -->
    defineReactive(obj, key, obj[key]);
  }
}

// Store the real attribute value
const _obj = {<!-- -->};
// Add responsiveness to each property
function defineReactive(obj, key, value) {<!-- -->
  _obj[key] = value;
  Object.defineProperty(obj, key, {<!-- -->
    // get value
    get() {<!-- -->
      console.log(`${<!-- -->key} value: ${<!-- -->_obj[key]}`);
      return _obj[key];
    },
    // store value
    set(newValue) {<!-- -->
      if (newValue === _obj[key]) return;
      console.log(`${<!-- -->key} stored value: changed from ${<!-- -->_obj[key]} to ${<!-- -->newValue}`);
      _obj[key] = newValue;
    },
  });
}

In the observer function, we only process object. We add independent responsiveness to each property of the object. The method is to use Object.defineProperty. It can modify the description of existing properties of an object. We change a normal property into a property described by getter and setter. In the get and set functions, you can monitor the access and changes of attribute values and process them. The real value is stored in another object. Let’s take a look at the usage effects.

import {<!-- --> observer } from './index.js'
const data = {<!-- -->
  a: 1,
  b: 2,
};
observer(data);

data.a = 'Hello'
data.b = 'js'
console.log(data.a + data.b);

We define several properties in the object, and then access the object through responsive processing.

# Output effect
a stored value: changed from 1 to hello
b storage value: changed from 2 to js
a value: hello
b value: js
hellojs

Use closures to optimize data storage

The above code defines an internal object to store the real data. The key of the internal object is the key of the responsive object. At this time, if multiple objects obtain responsiveness through this function and define attributes with the same key, the internal storage will be overwritten. So do we need to set up multiple storage variables to store data for different objects? No, we can do this simply using closures,

Closure means that when a function ends, its internal resources are not completely released and can continue to be used. Using this feature, we can store some variables in the function closure without defining an internal object for storage.

// Add responsiveness to each attribute
function defineReactive(obj, key, value) {<!-- -->
  Object.defineProperty(obj, key, {<!-- -->
    // get value
    get() {<!-- -->
      console.log(`${<!-- -->key} value: ${<!-- -->value}`);
      return value
    },
    // store value
    set(newValue) {<!-- -->
      if (newValue === value) return;
      console.log(`${<!-- -->key} stored value: changed from ${<!-- -->value} to ${<!-- -->newValue}`);
      value = newValue;
    },
  });
}

We use value as the key variable of the closure. At this time, even after the defineReactive function ends, because the internal get and set functions are still using the value variable, it will not be destroyed. When the set function is triggered to modify the attribute value, we directly change the value to the new value. When the subsequent get function takes the value, the new value can also be obtained.

Deep monitoring of nested objects

Our implemented observer function can only process one layer of attributes of an object. Our responsiveness is invalid for multi-layer nested objects, for example:

const data = {<!-- -->
  a: 1,
  b: {<!-- --> c: 2 },
};
observer(data);
data.b.c = "js";
// Output result:
// b value: [object Object]

At present, our code is only responsive when data.b takes a value, but when data.b.c is assigned a value, the responsiveness is gone. We handle the monitoring of nested objects:

// Add responsiveness to each attribute
function defineReactive(obj, key, value) {<!-- -->
  // If it is a nested object, continue to monitor its properties
  observer(value);
  Object.defineProperty(obj, key, {<!-- -->
    // get value
    get() {<!-- -->
      console.log(`${<!-- -->key} value: ${<!-- -->value}`);
      return value;
    },
    // store value
    set(newValue) {<!-- -->
      if (newValue === value) return;
      console.log(`${<!-- -->key} stored value: changed from ${<!-- -->value} to ${<!-- -->newValue}`);
      value = newValue;
    },
  });
}

The implementation is also very simple, that is, recursively monitor value in defineReactive, that is, if value is an object, continue to monitor its properties.

New object depth monitoring

Imagine this situation: a certain deep monitoring object attribute was not an object at first, but was later changed to an object. Or the nested object itself is replaced with another object. At this time, the nested object we changed is not responsive. for example:

import {<!-- --> observer } from "./index.js";
const data = {<!-- -->
  a: 1,
  b: 2
};
observer(data);

data.a = "Hello";
data.b = {<!-- --> c: 2 };
data.b.c = 'js';
data.b.d = 3;
console.log(data.a + data.b.c);

The output at this time is:

# Output effect
a stored value: changed from 1 to hello
b storage value: changed from 2 to [object Object]
b value: [object Object]
a value: hello
b value: [object Object]
hellojs

It can be seen that after modifying the b attribute to an object, the newly added c attribute was not monitored. The mechanism here is the same as Vue2. At this time, you can use Vue.set in Vue to add responsiveness to the new object. For our code, the implementation can be simpler: directly adapt to the responsive processing when adding new nested objects (but adding new attributes is still not possible). The change is also very simple, just increase the responsiveness to the new value of the object entering the set function.

set(newValue) {<!-- -->
  if (newValue === value) return;
  // If an object is stored, continue to increase responsiveness
  observer(newValue);
  console.log(`${<!-- -->key} stored value: changed from ${<!-- -->value} to ${<!-- -->newValue}`);
  value = newValue;
}

The output effect at this time is as follows: (Note that data.b.d is a new attribute, and responsiveness is still not added here).

# Output effect
a stored value: changed from 1 to hello
b storage value: changed from 2 to [object Object]
b value: [object Object]
c storage value: changed from 2 to js
a value: hello
b value: [object Object]
c value: js
hellojs

Add responsiveness to new attributes

In the previous section, our new attribute data.b.d did not increase responsiveness. In other words, attributes that were not defined in data at the beginning were not responsive. Vue provides the Vue.set method to add responsiveness to such new properties. Fortunately, our defineReactive function can also achieve this purpose. Let’s take a look at an example.

import {<!-- --> observer, defineReactive } from "./index.js";
const data = {<!-- -->
  a: 1,
  b: 2
};
observer(data);

data.a = "Hello";
data.b = {<!-- -->};
defineReactive(data.b, 'c', 2);
data.b.c = 'js';
defineReactive(data, 'd', 3);
data.d = 4;
console.log(data.a + data.b.c);

Regardless of whether it is a nested new attribute or a new attribute bound to data, you can use the defineReactive function to add responsiveness to it. Take a look at the output:

# Output effect
a stored value: changed from 1 to hello
b storage value: changed from 2 to [object Object]
b value: [object Object]
b value: [object Object]
c storage value: changed from 2 to js
d storage value: changed from 3 to 4
a value: hello
b value: [object Object]
c value: js
hellojs

Responsiveness of arrays

Using Object.defineProperty cannot monitor the changes caused by push and other methods of the array, so we have to process the array form separately. We repackage the prototype of the array and override the prototype method. This enables us to listen to the array before using methods to modify the array object.

//Inherit array prototype object
const arrProperty = Object.create(Array.prototype)
// Override some array prototype methods
const methods = ['push','pop','shift','unshift','splice']
methods.forEach(method => {<!-- -->
  arrProperty[method] = function () {<!-- -->
    console.log(`Array${<!-- -->method}method`);
    //Recall the corresponding array method
    Array.prototype[method].call(this, ...arguments);
  }
})

As you can see, we have rewritten the commonly used array methods. In the function content we rewritten, we first monitor the changes, and then re-call the real method to execute the change logic.

Then in the observer function, perform special processing on the array:

export function observer(obj) {<!-- -->
  // If it is not an object, return it as is
  if (!isObject(obj)) return obj;
  // If it is an array, modify the prototype
  if (Array.isArray(obj)) {<!-- -->
    obj.__proto__ = arrProperty
  }
  // Traverse the object and add responsiveness to each property
  for (const key in obj) {<!-- -->
    defineReactive(obj, key, obj[key]);
  }
}

Then we can monitor the responsiveness of the array method. Take a look at the example:

import {<!-- --> observer } from "./index.js";
const data = [1, 2]
observer(data);

data[1] = 3;
data.push(4);
console.log(data[2])

The output effect at this time is as follows: (The output of the get method is omitted in the following)

# Output effect
1Storage value: changed from 2 to 3
Array push method: 4
4

Summary

Here is just a brief understanding of some of Vue2’s responsive principles and simplified scenarios. In fact, Vue2’s responsive processing is much more complicated, and it also involves some methods of object communication and design patterns. When we have time later, we will discuss in more detail how to implement responsiveness in Vue2.

Reference

  • Object.defineProperty() MDN
    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  • In-depth reactive principles Vue 2.x documentation
    https://v2.cn.vuejs.org/v2/guide/reactivity.html
  • Vue responsiveness principle source code used in the article
    https://github.com/jzplp/VueJz
  • Vue2 & Vue3 responsive implementation principle
    https://juejin.cn/post/7253148953600262203
  • Interviewer: Can you write Vue responsiveness by hand? (Vue2 Responsive Principle [Full Version])
    https://juejin.cn/post/7079807948830015502