十六、数据的响应式本质


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

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

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

在 JS 中,要实现数据拦截,要么是 Object.defineProperty,要么是 Proxy,而这两者都是针对对象来进行操作的。

ref 以及 reactive 源码:

class RefImpl<T> {
  private _value: T
  private _rawValue: T

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

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 有可能是原始值,有可能是 reactive 返回的 proxy
    this._value = __v_isShallow ? value : toReactive(value)
  }
  // 可以把这里的get和set理解为Object.defineProperty
  get value() {
    // 收集依赖 略
    return this._value
  }

  set value(newVal) {
    // 略
  }
}

// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

// 回忆 ref 的用法
const state = ref(5);
state.value;
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // ...

  // 创建 Proxy 代理对象
  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,
  )
}

从源码中我们就可以看出,ref 和 reactive 在实现响应式上面的策略是有所不同

  • ref:使用 Object.defineProperty + Proxy 方式
  • reactive:使用 Proxy 方式

这节课还有一个非常重要的知识点,就是要 学会判断某个操作是否会产生拦截。因为只有产生拦截,才会有后续的依赖收集和派发更新一类的操作。

简单复习上节课的知识,有两个 API 能够实现拦截:

  1. Object.defineProperty
    • 特定的属性的读取
    • 特定的属性的赋值
  2. 操作 Proxy 代理对象的成员
    • 读取
    • 赋值
    • 新增
    • 删除

测试题目:

// demo1
let state = ref(1);
state; // 不会拦截
console.log(state); // 不会拦截
console.log(state.value); // 会拦截,因为访问了 value 属性
console.log(state.a); // 不会拦截
state.a = 3; // 不会拦截
state.value = 3; // 会拦截
delete state.value; // 不会拦截
state = 3; // 不会拦截
// demo2
let state = ref({ a: 1 });
state; // 不会拦截
console.log(state); // 不会拦截
console.log(state.value); // 会拦截
console.log(state.a); // 不会拦截
console.log(state.value.a); // 会拦截,拦截到 value 和 a 属性的 get 操作
state.a = 3; // 不会拦截
state.value.a = 3; // 会拦截,value 的 get 操作,a 属性的 set 操作
delete state.value.a; // 会拦截,value 的 get 操作,a 属性的 delete 操作
state.value = 3; // 会拦截,value 的 set 操作
delete state.value; // 不会拦截
state = 3; // 不会拦截
// demo3
let state = reactive({});
state; // 不会拦截
console.log(state); // 不会拦截
console.log(state.a); // 会拦截
state.a = 3; // 会拦截
state.a = {
  b: {
    c: 3,
  },
}; // 会拦截,拦截到 a 属性的 set 操作
console.log("-------------");
console.log(state.a.b.c); // 会拦截
delete state.a.b; // 会拦截 a 是 get 操作,b 是 delete 操作
// demo4
const state = ref({ a: 1 });
const k = state.value; 
console.log("-------------");
console.log(k); // 不会拦截,k 相当于是一个 proxy 对象,没有针对成员进行操作
k.a = 3; // 会拦截,因为 k 是一个 proxy 对象,对 k 的成员进行操作会触发代理的 set 操作
const n = k.a; // 会拦截,因为访问了 k 的成员 a,会触发代理的 get 操作
console.log("-------------");
console.log(n);
// demo5
const arr = reactive([1, 2, 3]);
arr; // 不会拦截
arr.length; // 会拦截
arr[0]; // 会拦截,拦截 0 的 get 操作
arr[0] = 3; // 会拦截,拦截 0 的 set 操作
arr.push(4); // 会被拦截

再次强调,一定要学会去判断针对一个对象进行操作的时候,是否会发生拦截,这一点非常重要‼️


文章作者: 吴俊杰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 吴俊杰 !
  目录