Vue的响应式代理和数据改造

12/25/2023 Vue

在 Vue 中,响应式代理是实现数据驱动的核心机制之一。当开发者改变 Vue 实例的数据时,视图会立即进行渲染,以显示最新的数据。以下是对 Vue 中响应式代理和数据改造的解析。

# 初始化阶段

在 Vue 的初始化阶段,会执行 initData ,对 data 属性进行改造。如果 data 是一个函数,会立即执行该函数获取 data 对象,并将其赋值给 this._data

// proxy data on instance
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
  const key = keys[i];
  // ...
  if (props && hasOwn(props, key)) {
    __DEV__ &&
      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);
  }
}

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

这段代码的作用是将 data 对象上的所有属性代理到 Vue 实例上,使得在 Vue 组件中可以通过 this.xxx 的方式获取 data 属性上的值。实际上, this.xxx 的访问路径是: this.xxx → this._data.xxx → data对象

代理完之后,就要对 data 对象进行响应式改造,递归 data 对象,如果 data 的属性中有对象或者数组的,则继续对其进行响应式改造。

# 对象的响应式改造

对于对象的响应式改造,首先会创建一个 dep 对象, 让其跟随 Observer 对象, dep 的作用是监听该对象是否发生变化。

然后遍历属性,为每个属性定义一个 dep ,用于监听属性变化。通过 Object.defineProperty 改造 getset 方法(vue3 通过 proxy ),当视图取值时,进行监听;当修改属性时,通知视图更新。

dep 的作用可以看这篇文章

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val;
    if (Dep.target) {
      if (__DEV__) {
        dep.depend({
          target: obj,
          type: TrackOpTypes.GET,
          key,
        });
      } else {
        dep.depend();
      }
      if (childOb) {
        childOb.dep.depend();
        if (isArray(value)) {
          dependArray(value);
        }
      }
    }
    return isRef(value) && !shallow ? value.value : value;
  },
  set: function reactiveSetter(newVal) {
    const value = getter ? getter.call(obj) : val;
    if (!hasChanged(value, newVal)) {
      return;
    }
    if (__DEV__ && customSetter) {
      customSetter();
    }
    if (setter) {
      setter.call(obj, newVal);
    } else if (getter) {
      // #7981: for accessor properties without setter
      return;
    } else if (!shallow && isRef(value) && !isRef(newVal)) {
      value.value = newVal;
      return;
    } else {
      val = newVal;
    }
    childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock);
    if (__DEV__) {
      dep.notify({
        type: TriggerOpTypes.SET,
        target: obj,
        key,
        newValue: newVal,
        oldValue: value,
      });
    } else {
      dep.notify();
    }
  },
});

# 数组改造

对于数组,Vue 并没有对数组中所有元素进行监听改造,而是只对数组的增删 API 和数组中的对象元素进行改造。具体的方法如下:

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

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method];
  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;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    if (__DEV__) {
      ob.dep.notify({
        type: TriggerOpTypes.ARRAY_MUTATION,
        target: this,
        key: method,
      });
    } else {
      ob.dep.notify();
    }
    return result;
  });
});

def(value, '__ob__', this);
if (isArray(value)) {
  if (!mock) {
    if (hasProto) {
      /* eslint-disable no-proto */
      (value as any).__proto__ = arrayMethods;
      /* eslint-enable no-proto */
    } else {
      for (let i = 0, l = arrayKeys.length; i < l; i++) {
        const key = arrayKeys[i];
        def(value, key, arrayMethods[key]);
      }
    }
  }
  if (!shallow) {
    this.observeArray(value);
  }
}

这段代码的作用是对数组的常用操作方法(如 pushpopshift 等)进行了改造,使得修改数组时能够通知视图更新。

对于这些实现,其实不能覆盖所有情况。

例如以下这些情况不会更新。

  1. 修改数组上的元素, a = [1, 2, 3], a[0] = 100
  2. 新增对象属性, a = { b: 1 }, a.c = 1

官方则通过 $set 去设置,内部原理其实就是修改完值之后,会调用一次 a.__ob__.dep.notify() ,主动刷新视图

# 总结

通过响应式代理和数据改造,Vue 实现了数据的双向绑定。当数据发生变化时,视图会自动更新,保持数据和视图的同步。这是 Vue 实现响应式的核心机制,也是 Vue 在前端框架中的一大特色。