vue3-reactive的简易实现(1)

115 阅读4分钟

Vue3对比Vue2的变化

  • 在Vue2的时候使用defineProperty来进行数据的劫持, 需要对属性进行重写添加gettersetter 性能差
  • 当新增属性和删除属性时无法监控变化。需要通过$set$delete实现
  • 数组不采用defineProperty来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理

Vue3中使用Proxy来实现响应式数据变化。从而解决了上述问题。

Proxy

我这边创建了一个仓库, 里面开发环境已经搭建好了, 直接拿来用就可以了, pnpm可以看这里

npm install -g pnpm // 全局安装一下pnpm

pnpm install -w

在packages/reactivity/src目录下创建一个 reactive.ts 先简单处理下对象的代理

import { isObject } from "@vue/shared";

// 1 将数据转化为响应式对象,并且只能做对象的代理
export function reactive(target) {
  // 1.1 如果不是对象直接就return掉
  if (!isObject(target)) {
    return;
  }

  // 1.2 利用proxy做一个代理,并没有重新代理属性,而是访问代理对象时会走get,赋值时走set
  const proxy = new Proxy(target, {
    get(target, p, receiver) {
      console.log(p)
      return target[p];
    },
    set(target, p, value, receiver) {
      target[p] = value;
      return true;
    },
  });

  return proxy;
}

这样就完成了一个对象最基本的代理功能, 下面在packages/reactivity/src目录下创建一个index.ts 导出reactive

export { reactive } from "./reactive";

运行 pnpm dev, 可以看到packages/reactivity/dist里面有两个文件, reactivity.global.js 和reactivity.global.js.map, 我们在dist 文件夹里面再创建一个 index.html 文件, 引入打包后的文件, 查看是否有reactive函数存在

<script src="./reactivity.global.js"></script>
<script>
  const { reactive } = VueReactivity;
  let wbw = {
    name: "wbw",
    age: 7,
    address: { province: "gd", city: "gz" },
  };
  let wbwProxy = reactive(wbw);
  console.log("wbwProxy", wbwProxy);
  wbwProxy.name
  wbwProxy.age
</script>

运行正常, 访问name和age都可以正常打印处理, 但是还有一种情况, 利用存取描述符get, set进行读取和设置Object.defineProperty() 可以了解一下

let wbw = {
  name: "wbw",
  age: 7,
  address: { province: "gd", city: "gz" },
  get myName() {
    return this.name;
  },
};
let wbwProxy = reactive(wbw);

// 这时候访问myName的时我们预期是希望打印出myName和name的,
// 但是上面的只会打印一次myName,而this.name的时候this指向的是wbw本身而不是代理对象,
// 所以没有走get,满足不了我们的预期
wbwProxy.myName 

这个时候就需要用到 Reflect了, 不熟悉的朋友可以先了解一下, 下面将代码进行一个改造

const proxy = new Proxy(target, {
  get(target, p, receiver) {
    console.log(p);
    return Reflect.get(target, p, receiver);
  },
  set(target, p, value, receiver) {
    return Reflect.set(target, p, value, receiver);
  },
});

这样就可以满足我们的预期了

缓存处理

接下来看下面的代码

const data1 = reactive(wbw);
const data2 = reactive(wbw);
console.log(data1 === data2); // false

代理相同的对象, 结果每次都是不同的实例产生, 我们希望代理相同对象的时候能返回的是同一个实例对象, 接下来看怎么处理一下, 这里用到了 WeakMap

import { isObject } from "@vue/shared";

// 2 利用WeakMap做缓存处理
const reactiveMap = new WeakMap()
// 1 将数据转化为响应式对象,并且只能做对象的代理
export function reactive(target) {
  // 1.1 如果不是对象直接就return掉
  if (!isObject(target)) {
    return;
  }

  // 2.2 访问的时候先看下reactiveMap是否已经存在代理, 如果是就直接返回
  let existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  // 1.2 利用proxy做一个代理,并没有重新代理属性,而是访问代理对象时会走get,赋值时走set
  const proxy = new Proxy(target, {
    get(target, p, receiver) {
      return Reflect.get(target, p, receiver);
    },
    set(target, p, value, receiver) {
      return Reflect.set(target, p, value, receiver);
    },
  });

  // 2.1 每次都将target对象作为key将代理对象缓存起来
  reactiveMap.set(target, proxy);
  return proxy;
}

这样再看打印结果就是true了, 还有一种情况, 如果代理对象已经代理过了, 那么就直接返回

const data1 = reactive(wbw);
const data2 = reactive(data1);
console.log(data1 === data2); // false
import { isObject } from "@vue/shared";
// 3 给代理对象增加一个特殊的属性
const enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}
// 2 利用WeakMap做一个缓存处理
const reactiveMap = new WeakMap();
// 1 将数据转化为响应式对象, 并且只能做对象的代理
export function reactive(target) {
  // 1.1 如果不是对象直接就return掉
  if (!isObject(target)) {
    return;
  }

  // 3.1 如果是普通对象, 必然没有get, 这里排除故意添加重名属性的
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }

  // 2.2 访问的时候先看下reactiveMap是否已经存在代理, 如果是就直接返回
  let existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  // 1.2 利用proxy做一个代理,并没有重新代理属性,而是访问代理对象时会走get,赋值时走set
  const proxy = new Proxy(target, {
    get(target, key, receiver) {

      // 3.2 如果已经是代理对象了, 那么访问该key的时候就会进去这里
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
      }

      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      return Reflect.set(target, key, value, receiver);
    },
  });

  // 2.1 每次都将target对象为key将代理对象缓存起来
  reactiveMap.set(target, proxy);

  return proxy;
}

代码抽离

这个时候来做一个抽离, 把 ReactiveFlags 和 proxy handle抽到 packages/reactivity/src/baseHandle.ts 文件中

// reactive.ts
import { isObject } from "@vue/shared";
import { ReactiveFlags, mutabelHandles } from "./baseHandle"; // 引入


const proxy = new Proxy(target, mutabelHandles); // 修改这一行

// baseHandle.ts
export const enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}

export const mutabelHandles = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }

    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver);
  },
};

深度代理

import { isObject } from "@vue/shared";
import { reactive } from "./reactive";

export const mutabelHandles = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }

    // 4 当取值时返回的值是对象,则返回这个对象的代理对象,从而实现深度代理
    const res = Reflect.get(target, key, receiver);
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver);
  },
};

后续有时间会继续更新这个系列下去的......