Detailed explanation of data hijacking example of handwritten Vue source code

  • 2021-10-16 00:46:16
  • OfStack

Source code: Portal

Vue intercepts the data we pass in from data:

Object: Recursively sets the get/set method for each property of the object Array: Modify the prototype method of the array, and override the method that would modify the original array

When users set values, modify values and call methods to modify the original array for objects in data, they can add some logic to deal with them, and update the data update page at the same time.

Responsive expression in Vue (reactive): Object property or array method is intercepted, and the view can be automatically updated when the property or array is updated. The data observed in the code is responsive

Create an Vue instance

Let's first let the code implement the following functions:


<body>
<script>
 const vm = new Vue({
 el: '#app',
 data () {
 return {
 age: 18
 };
 }
 });
 //  Will trigger age Property corresponding to the set Method 
 vm.age = 20;
 //  Will trigger age Property corresponding to the get Method 
 console.log(vm.age);
</script>
</body>

In src/index. js, define the constructor of Vue. The Vue used by the user is the Vue exported here:


import initMixin from './init';

function Vue (options) {
 this._init(options);
}

//  Prototype method extension 
initMixin(Vue);
export default Vue;

In init, the _ init method on the prototype is defined and the state is initialized:


import initState from './state';

function initMixin (Vue) {
 Vue.prototype._init = function (options) {
 const vm = this;
 //  Put the options passed in by the user into vm.$options After that, it is very convenient to pass the example vm To access all the options passed in when instantiated 
 vm.$options = options;
 initState(vm);
 };
}

export default initMixin;

In the _ init method, all options is put into vm. $options, which not only makes it easier to get the configuration items passed in by the user in the later code, but also allows the user to get some custom options passed in during instantiation through this api. For example, in Vuex and Vue-Router, the router and store attributes passed in during instantiation can be obtained through $options.

In addition to setting vm. $options, the initState method is executed in _ init. In this method, the attributes passed in the options will be judged to initialize the configuration items such as props, methods, data, watch and computed respectively. Here, we mainly deal with data options:


import { observe } from './observer';
import { proxy } from './shared/utils';

function initState (vm) {
 const options = vm.$options;
 if (options.props) {
 initProps(vm);
 }
 if (options.methods) {
 initMethods(vm);
 }
 if (options.data) {
 initData(vm);
 }
 if (options.computed) {
 initComputed(vm)
 }
 if (options.watch) {
 initWatch(vm)
 }
}

function initData (vm) {
 let data = vm.$options.data;
 vm._data = data = typeof data === 'function' ? data.call(vm) : data;
 //  Right data Intercept the data in the 
 observe(data);
 //  Will data Agent the properties in to the vm Upper 
 for (const key in data) {
 if (data.hasOwnProperty(key)) {
 //  For vm Agent owned data You can use the properties in the vm.xxx To get 
 proxy(vm, key, data);
 }
 }
}

export default initState;

The following is done in initData:

data may be an object or a function, where data Series 1 is treated as an object Observe the data in data, add set/get methods for all object attributes, and override the prototype chain method of the array Proxy the attributes in data to vm, so that users can access the corresponding values directly through the instance vm instead of through vm._data

Create a new src/observer/index. js, where you write the logic of the observe function:


function observe (data) {
 //  If it is an object, it traverses every 1 Elements 
 if (typeof data === 'object' && data !== null) {
 //  Values that have been observed are no longer processed 
 if (data.__ob__) {
 return;
 }
 new Observer(data);
 }
}

export { observe };

The observe function will filter the data in data, and only process objects and arrays. The real processing logic is in Observer:


/**
 *  For data Settings for all objects in the `set/get` Method 
 */
class Observer {
 constructor (value) {
 this.value = value;
 //  For data Every one in 1 Objects and arrays are added __ob__ Attribute, it is convenient to pass directly through data To directly call the property in the Observer Properties and methods on an instance 
 defineProperty(this.value, '__ob__', this);
 //  Arrays and objects are treated separately, because every 1 All indexes are set get/set The performance consumption of the method is relatively large 
 if (Array.isArray(value)) {
 Object.setPrototypeOf(value, arrayProtoCopy);
 this.observeArray(value);
 } else {
 this.walk();
 }
 }

 walk () {
 for (const key in this.value) {
 if (this.value.hasOwnProperty(key)) {
 defineReactive(this.value, key);
 }
 }
 }

 observeArray (value) {
 for (let i = 0; i < value.length; i++) {
 observe(value[i]);
 }
 }
}

It should be noted that the __ob__ property should be set to non-enumerable, otherwise an infinite loop may be raised after the object is traversed

The __ob__ attribute is added to both the object and array in the Observer class, and then the Observer instance can be obtained directly from the object and array vm. value.__ob__ in data.

When the incoming value is an array, it takes a lot of performance to observe every index of the array, and in actual use, we may only operate on the first and last item of the array, namely arr [0], arr [arr. length-1], and rarely write the code of arr [23] = xxx.

So we chose to override the method of the array, point the prototype of the array to the newly created object arrayProtoCopy that inherits Array. prototype, and continue to observe every item in the array.

The logic for creating an array prototype in data is in src/observer/array. js:


// if (Array.isArray(value)) {
// Object.setPrototypeOf(value, arrayProtoCopy);
// this.observeArray();
// }
const arrayProto = Array.prototype;
export const arrayProtoCopy = Object.create(arrayProto);

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

methods.forEach(method => {
 arrayProtoCopy[method] = function (...args) {
 const result = arrayProto[method].apply(this, args);
 console.log('change array value');
 // data The array in will call the method defined here, this Point to the array 
 const ob = this.__ob__;
 let inserted;
 switch (method) {
 case 'push':
 case 'unshift':
 inserted = args;
 break;
 case 'splice': // splice(index,deleteCount,item1,item2)
 inserted = args.slice(2);
 break;
 }
 if (inserted) {ob.observeArray(inserted);}
 return result;
 };
});

With the Object. create method, you can create a new object arrayProtoCopy with the prototype Array. prototype. Modifying the seven methods of the original array will be set as private properties of the new object, and the corresponding methods on arrayProto will be called at execution time.

After this processing, you can add your own logic before and after the execution of the methods in arrayProto, and other methods except these seven methods will use the corresponding methods on arrayProto according to the prototype chain without any additional processing.

In the method of modifying the original array, the following additional logic is added:


const ob = this.__ob__;
let inserted;
switch (method) {
 case 'push':
 case 'unshift':
 inserted = args;
 break;
 case 'splice': // splice(index,deleteCount,item1,item2)
 inserted = args.slice(2);
 break;
}
if (inserted) {ob.observeArray(inserted);}

push, unshift, and splice add elements to the array, and observe the added elements. Here, we use the __ob__ attribute added to the array in Observer to directly call ob. observeArray and continue to observe the newly added elements in the array.

For an object, iterate through every 1 property of the object to add set/get methods to it. If the property of an object is still an object, it will be processed recursively


function defineReactive (target, key) {
 let value = target[key];
 //  Continue to value Monitor, if value If it is still an object, it will continue new Observer , execution defineProperty To set the get/set Method 
 //  Otherwise, it will be in observe Do nothing in the method 
 observe(value);
 Object.defineProperty(target, key, {
 get () {
 console.log('get value');
 return value;
 },
 set (newValue) {
 if (newValue !== value) {
 //  The newly added element may also be an object. Continue to set the properties of the newly added object get/set Method 
 observe(newValue);
 //  Writing like this will be new value Point 1 A new value without affecting target[key]
 console.log('set value');
 value = newValue;
 }
 }
 });
}

class Observer {
 constructor (value) {
 // some code ...
 if (Array.isArray(value)) {
 // some code ...
 } else {
 this.walk();
 }
 }

 walk () {
 for (const key in this.value) {
 if (this.value.hasOwnProperty(key)) {
 defineReactive(this.value, key);
 }
 }
 }

 // some code ... 
}

Problems in data observation

Considerations for Detecting Changes

Let's create a simple example first:


const mv = new Vue({
 data () {
 return {
 arr: [1, 2, 3],
 person: {
 name: 'zs',
 age: 20
 }
 }
 }
})

For an object, we just intercept its value taking and assignment operations, and adding and deleting values do not intercept:


import initMixin from './init';

function Vue (options) {
 this._init(options);
}

//  Prototype method extension 
initMixin(Vue);
export default Vue;

0

For arrays, modifying values with indexes and modifying array length will not be observed:


import initMixin from './init';

function Vue (options) {
 this._init(options);
}

//  Prototype method extension 
initMixin(Vue);
export default Vue;

1

To handle the above situation, Vue provides users with $set and $delete methods:

$set: Adds 1 property to the responsive object, ensuring that the new property is also responsive, thus triggering a view update $delete: Delete 1 property on the object. If the object is responsive, make sure that the deletion triggers the view update.

Conclusion

By realizing the data hijacking of Vue, we will have a deeper understanding of the data initialization and response formula of Vue.

At work, we may always wonder, why did I update the value, but the page didn't change? Now we can understand it from the perspective of source code, so as to know more clearly the problems existing in the code and how to solve and avoid these problems.

The directory structure of the code refers to the source code, so after reading the article, you can also find out the corresponding code from the source code to read, I believe you will have a different understanding!


Related articles: