In-depth understanding of Vue's responsive principle

Vue Responsive

Anyone who has used the Vue framework should know that data-driven is the core of the Vue framework, and two-way data binding is a major feature of it. According to the official explanation, we can clearly understand the simple principle of responsiveness.

The responsive principle of Vue2

When you pass an ordinary JavaScript object into a Vue instance as a data option, Vue will traverse all properties of this object, and use Object.defineProperty to convert all these properties into getters/setters. Object.defineProperty is a feature that cannot be shimmed in ES5, which is why Vue does not support IE8 and lower versions of browsers.

These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when the property is accessed and modified. It should be noted here that different browsers format the getter/setter differently when printing data objects on the console, so it is recommended to install vue-devtools to obtain a more friendly user interface for checking data.

Each component instance corresponds to a watcher instance, which will record the "touched" data property as a dependency during the rendering process of the component. Then when the dependency's setter fires, the watcher is notified, causing its associated component to re-render.

The responsive principle of Vue3

Implementation principle:
through Proxy (proxy): Intercept the change of any attribute in the object, including: reading and writing of attribute values, adding attributes, deleting attributes, etc.

Through Reflect (reflection): operate on the properties of the source object

new Proxy(data,{
    
    
  //拦截读取属性值
  get(target, prop){
    
    
    return Reflect.get(target, prop)
  },
  //拦截设置属性值或添加新属性
  set(target, prop, value){
    
    
    return Reflect.set(target, prop, value)
  },
  //拦截删除属性
  deleteProperty(target, prop){
    
    
    return Reflect.deleteProperty(target, prop)
  }
})

The responsive principles of Vue2 and Vue3 are actually similar, but Vue3’s proxy encapsulation and independence are relatively stronger and more flexible, but what we see is only the simplest and most basic responsive principle. If To get a deeper understanding of how Vue uses this principle to implement various two-way binding and data rendering operations in the framework, we can analyze its source code.

In-depth understanding of responsiveness (starting from instantiation)

1. Data initialization

new Vue({
    
    
  el: "#app",
  router,
  store,
  render: (h) => h(App),
});

This code must be very familiar to everyone. This is the process of Vue instantiation. From the new operator, we can see that Vue is actually a constructor, nothing special. The parameter passed in is an object, which we call options (options).

// src/index.js

import {
    
     initMixin } from "./init.js";

// Vue就是一个构造函数 通过new关键字进行实例化
function Vue(options) {
    
    
  // 这里开始进行Vue初始化工作
  this._init(options);
}
// _init方法是挂载在Vue原型的方法 通过引入文件的方式进行原型挂载需要传入Vue
// 此做法有利于代码分割
initMixin(Vue);
export default Vue;

Because many things may be processed in Vue initialization, such as data processing, event processing, life cycle processing, etc., dividing the introduction of different files is conducive to code segmentation.

// src/init.js
import {
    
     initState } from "./state";
export function initMixin(Vue) {
    
    
  Vue.prototype._init = function (options) {
    
    
    const vm = this;
    // 这里的this代表调用_init方法的对象(实例对象)
    //  this.$options就是用户new Vue的时候传入的属性
    vm.$options = options;
    // 初始化状态
    initState(vm);
  };
}

initMixin mounts the _init method on the Vue prototype for Vue instances to call.

// src/state.js
import {
    
     observe } from "./observer/index.js";

export function initState(vm) {
    
    
  // 获取传入的数据对象
  const opts = vm.$options;
  if (opts.props) {
    
    
    initProps(vm);
  }
  if (opts.methods) {
    
    
    initMethod(vm);
  }
  if (opts.data) {
    
    
    // 初始化data
    initData(vm);
  }
  if (opts.computed) {
    
    
    initComputed(vm);
  }
  if (opts.watch) {
    
    
    initWatch(vm);
  }
}

// 初始化data数据
function initData(vm) {
    
    
  let data = vm.$options.data;
  //   实例的_data属性就是传入的data
  // vue组件data推荐使用函数 防止数据在组件之间共享
  data = vm._data = typeof data === "function" ? data.call(vm) : data || {
    
    };

  // 把data数据代理到vm 也就是Vue实例上面 我们可以使用this.a来访问this._data.a
  for (let key in data) {
    
    
    proxy(vm, `_data`, key);
  }
  // 对数据进行观测 --响应式数据核心
  observe(data);
}
// 数据代理
function proxy(object, sourceKey, key) {
    
    
  Object.defineProperty(object, key, {
    
    
    get() {
    
    
      return object[sourceKey][key];
    },
    set(newValue) {
    
    
      object[sourceKey][key] = newValue;
    },
  });
}

①Through this code, we can get a piece of information that is very helpful to us when developing Vue projects, that is, the order of data initialization is prop>methods>data>computed>watch. Regarding the question of whether we can call the value of prop in data, if we know the order of data rendering, we can solve it easily.

②In addition, through this source code, we can also get a piece of information. The data is encapsulated with a function instead of an object object. This is to avoid data sharing between components, so that each of our components can have an independent variable scope.

2. Object data hijacking
Object data hijacking is actually easy to understand. The code adds a corresponding listener to each parameter in the object through recursion, so when the object data changes, it will naturally trigger listener.

Here we can get a piece of information that the object is only monitored during the initialization phase. When we add parameters to the object later, we must use the built-in functions set and set provided by Vue .Only set and delete can perform dynamic operations on object parameters. Otherwise, adding new parameters directly through Object.xxx will not have the effect of two-way binding at this time.

// src/obserber/index.js
class Observer {
    
    
  // 观测值
  constructor(value) {
    
    
    this.walk(value);
  }
  walk(data) {
    
    
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
    
    
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
    
    
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
  Object.defineProperty(data, key, {
    
    
    get() {
    
    
      console.log("获取值");
      return value;
    },
    set(newValue) {
    
    
      if (newValue === value) return;
      console.log("设置值");
      value = newValue;
    },
  });
}
export function observe(value) {
    
    
  // 如果传过来的是对象或者数组 进行属性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    
    
    return new Observer(value);
  }
}

array monitor

// src/obserber/index.js
import {
    
     arrayMethods } from "./array";
class Observer {
    
    
  constructor(value) {
    
    
    if (Array.isArray(value)) {
    
    
      // 这里对数组做了额外判断
      // 通过重写数组原型方法来对数组的七种方法进行拦截
      value.__proto__ = arrayMethods;
      // 如果数组里面还包含数组 需要递归判断
      this.observeArray(value);
    } else {
    
    
      this.walk(value);
    }
  }
  observeArray(items) {
    
    
    for (let i = 0; i < items.length; i++) {
    
    
      observe(items[i]);
    }
  }
}

The monitoring of the array is to judge each element of the array. If the array also contains an array, it needs to be monitored recursively. If it is not an array element, the operation of monitoring and setting the array is directly performed.

Because the interception of array subscripts is too wasteful of performance, the data parameters passed in by the Observer constructor increase the judgment of the array.

// src/obserber/index.js
class Observer {
    
    
  // 观测值
  constructor(value) {
    
    
    Object.defineProperty(value, "__ob__", {
    
    
      //  值指代的就是Observer的实例
      value: this,
      //  不可枚举
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
}

Finally, in order to facilitate our operations on arrays, Vue rewrites some common methods of arrays. When we call these methods, the bottom layer of Vue will automatically add corresponding listeners for us, so we don’t need to render data and render elements again. bound.

// src/obserber/array.js
// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "reverse",
  "sort",
];
methodsToPatch.forEach((method) => {
    
    
  arrayMethods[method] = function (...args) {
    
    
    //   这里保留原型方法的执行结果
    const result = arrayProto[method].apply(this, args);
    // 这句话是关键
    // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)  this就是a  ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
    const ob = this.__ob__;

    // 这里的标志就是代表数组有新增操作
    let inserted;
    switch (method) {
    
    
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
    if (inserted) ob.observeArray(inserted);
    // 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
    return result;
  };
});

Guess you like

Origin blog.csdn.net/huangzhixin1996/article/details/130189342