Skip to content

响应式数据的本质

什么是响应式数据?其实就是 被拦截的对象

当对象被拦截之后,针对对象的各种操作也就能够被拦截下来,从而让我们有机会做一些额外的事情。因此只要是被拦截了的对象,就可以看作是一个响应式数据。

在 Vue3 中,创建响应式数据的方式有 refreactive 两种。这两个 API 的背后,就是针对对象添加拦截。

ref 以及 reactive 源码
ts
class RefImpl<T> {
  private _value: T;
  private _rawValue: T;

  public dep?: Dep = undefined;
  public readonly __v_isRef = true;

  constructor(
    value: T,
    public readonly __v_isRef: boolean
  ) {
    this._rawValue = __v_isShallow ? value : toRaw(value);
    this._value = __v_isShallow ? value : toReactive(value);
  }

  get value() {
    // 依赖收集
    // ...
    return this._value;
  }

  set value(newVal) {
    // ...
  }
}

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value;
ts
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}

export function reactive(target: Object) {
  // ...
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

从源码中我们就可以看出, refreactive 在实现响应式上面的策略是有所不同的——前者是通过 Object.definePropertyProxy,后者是通过使用 Proxy

理解响应式数据本质,还要学会 判断某个操作是否会产生数据拦截。只有产生数据拦截,才会触发后续的依赖收集和派发更新。

判断哪些操作会被拦截
JavaScript
class RefImpl {
  #value;
  constructor(value) {
    this.#value = isObject(value) ? reactive(value) : value;
  }

  get value() {
    console.log('get value');
    return this.#value;
  }

  set value(newValue) {
    console.log('set value');
    this.#value = isObject(newValue) ? reactive(newValue) : newValue;
  }
}

function isObject(value) {
  return value !== null && typeof value === 'object';
}
function deepProxy(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('get', key);
      const value = target[key];
      return isObject(value) ? deepProxy(value) : value;
    },
    set(target, key, value) {
      console.log('set', key);
      target[key] = isObject(value) ? deepProxy(value) : value;
      return true;
    },
    deleteProperty(target, key) {
      console.log('delete', key);
      delete target[key];
      return true;
    }
  });
}

const ref = (value) => new RefImpl(value);
const reactive = (obj) => deepProxy(obj);

state = ref(1);
state; // 不会拦截
console.log(state); // 不会拦截
console.log(state.value); // get value ---> 1
console.log(state.a); // 不会拦截
state.a = 2; // 不会拦截
state.value = 3; // set value
delete state.value; // 不会拦截
state = 3; // 不会拦截

state = ref({ a: 1 });
state; // 不会拦截
console.log(state); // 不会拦截 ---> { value: { a: 1 } }
console.log(state.value); // get value ---> { a: 1 }
console.log(state.a); // 不会拦截
console.log(state.value.a); // get value ---> get a ---> 1
state.a = 2; // 不会拦截
state.value.a = 3; // get value ---> set a
delete state.value.a; // delete a
state.value = 3; // set value
state = 3; // 不会拦截

state = reactive({});
state; // 不会拦截
console.log(state); // 不会拦截
console.log(state.a); // get a ---> undefined
state.a = 1; // set a
state.a = {
  b: {
    c: 100
  }
}; // set a
console.log('====================');
console.log(state.a.b.c); // get a ---> get b ---> get c ---> 100
delete state.a.b; // get a ---> delete b

state = ref({ a: 1 });
const temp = state.value; // get value
console.log(temp); // { a: 1 }
temp.a = 2; // set a
const temp2 = temp.a; // get a
console.log(temp2); // 2

arr = reactive([1, 2, 3]);
arr; // 不会拦截
arr[0]; // get 0
arr.length; // get length
arr[0] = 100; // set 0
arr.push(4); // get push ---> get length ---> set 3 ---> set length

提示

两段代码各自输出的原因,可以参考 Vue 中数据拦截的本质