Vue3 - 实现 reactive

224 阅读3分钟

Vue3 - 实现 reactive

实现创建响应式对象 API - reactive

使用

  • 使用 reactive 创建一个响应式对象
const obj = { name: "jj", age: 20 }
const state = reactive(obj)

核心API - Proxy

  • vue3 采用了 ES6 新增的 Proxy 来代理对象实现响应式

下例中:handler 的 get、set 中的 receiver 参数其实就是 proxy 本身

recevier 在代理中起到至关重要的作用

const handler = {
  get: function (target, property, receiver) {},
  set: function (target, property, value, receiver) {},
}
var p = new Proxy(target, handler) // receiver 是 p

receiver 和 Reflect

MDN 中说明:receiver 是一个Proxy 或者继承 Proxy 的对象

receiver 最主要的作用是用来解决 this 问题,为了方便理解,我们先来看一个例子

let person = {
  name: "jj",
  get aliasName() {
    return "*" + this.name + "*";
  },
  set aliasName(value) {
    this.name = value;
  },
};
const proxyPerson = new Proxy(person, {
  get(target, key, receiver) { // 取值
    return target[key]; // target是 person
  },
});

proxyPerson.aliasName; // 只触发了 aliasName 获取操作,没有触发 name 操作

// 页面和数据是有对应关系的 数据变化了要更新视图
proxyPerson.name = "xxx";

上面我们使用 Proxy 代理了 person

  • 假如我在视图中使用 aliasName 这个变量,会有 aliasName 对应的页面,但是并没有创造 name 和页面的关系

这意味着,我们执行 proxyPerson.aliasName 时只触发了 aliasName 获取操作,没有触发 name 操作

  • 页面和数据是有对应关系的,当数据变化时我们要更新视图

    proxyPerson.name = "xxx",此时 name 改变,但是由于没有创造 name 和页面的关系,视图不会更新

为了解决这个问题,这个时候就需要用到 receiver 了,除此之外还用到一个新的 API - Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法

Reflect.get() 方法,如果target对象中指定了getreceiver则为get调用时的`this值

所以 person 的代理改为以下方式

get(target, key, receiver) {
  console.log("取值", key);
  return Reflect.get(target, key, receiver);
},

处理完后,如果在 get 中 console 一下你会发现打印了两次,一次是读取 aliasName 一次是读取 name

实现 reactive

学习前面的知识后,我们就可以开始实现 reactive 了

function isObject(value) {
  return value !== null && typeof value === 'object'
}

const mutableHandlers = {
  // 这里的 receiver 就是 下面的 proxy
  get(target, key, receiver) {
    // 我们在使用 proxy 的时候要搭配 receiver 来使用,用来解决 this 问题
    
    if (isObject(target[key])) {
      // 如果取值的时候是对象,再次进行代理,返回代理后的结果
      return reactive(target[key])
    }
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) { // 更新
    return Reflect.set(target, key, value, receiver);
  }
}

function reactive(target) {
  if (!isObject(target)) return target
  
  // 缓存
  // ...
  
  // 如果对象被代理过,说明已经被 proxy 拦截过了
  // ...

  const proxy = new Proxy(target, mutableHandlers)
  return proxy
}

看到这里,是不是觉得 reactive 也就这么回事,其实不然,我们来考虑两个场景:

  • 场景一,思考一下打印结果是什么
const obj = { name: "jj", age: 20 };
const state1 = reactive(obj);
const state2 = reactive(obj)
console.log(state1 === state2);

答案是 false,虽然我们代理的对象相同,但是返回的 proxy 是不一样的

  • 场景二,代理代理过的对象
const obj = { name: "jj", age: 20 };
const state1 = reactive(obj);
const state3 = reactive(state1)

在 v3 中,已经代理过的对象不会再被代理

  1. vue3.0 会创建一个反向映射表 { 代理的结果:原内容 }
  2. 经过迭代去除了反向映射表;如果对象被代理过,说明已经被 proxy 拦截过了,就不再进行代理

代理缓存 - WeakMap

场景一续集

  • 在 vue3 中,上面打印结果是 true,原因是 v3 做了一个缓存机制,其实就是一个映射表
  • 其中用到了 WeakMap 弱引用
// 缓存
const reactiveMap = new WeakMap()
let existingProxy = reactiveMap.get(target) // 看一下这个对象是否被代理过
if (existingProxy) return existingProxy

ReactiveFlags

场景二续集

  • 如果对象被代理过,说明已经被 proxy 拦截过了,就不再进行代理
const enum ReactiveFlags {
  IS_REACTIVE = "__v__isReactive"
}
if (target[ReactiveFlags.IS_REACTIVE]) {
  return target
}

总结

  • reactive 只能处理对象类型的数据
  • 代理过的对象会使用映射表进行缓存,防止重复代理
  • 已经代理过的对象不会再被代理