Vue3.x | Proxy实现响应式系统

514 阅读3分钟

概述

Vue2.x版本中,实现响应式的原理是使用了Object.defineProperty将属性转为getter/setter。这样使得Vue能追踪依赖,当属性被访问或修改的时候,通知并重新渲染视图。

Proxy有什么优势?

proxyAPI能够创建一个虚拟的对象来表示原始数据,并且在handler参数里提供了set(),get()deleteProperty等方法。当原始数据被访问和修改的时候,会拦截到这些操作。它可以让我们从这些限制中解放:

  • 使用Vue.$set()去添加新的属性和使用Vue.$delete删除属性。
  • 监听数组的改变。

实现一个ES5版本的响应式系统

模拟Vue里的data选项

// 原始数据
let data = {
  price: 10,
  quantity: 2,
};

// 在Vue用来存储Watcher,在这里存储一个回调函数
let target = null;

定义一个依赖收集器,存储所有的订阅者

// 依赖收集器
class Dep {
  constructor() {
    this.subsribers = [];
  }
  // 添加依赖
  depend() {
    if (target && !this.subsribers.includes(target)) {
      this.subsribers.push(target);
    }
  }
  // 通知所有订阅者
  notify() {
    this.subsribers.forEach((sub) => {
      sub();
    });
  }
}

使用defineProperty将属性转为getter/setter

// 把传入的data转为getter/setter
Object.keys(data).forEach((key) => {
  const dep = new Dep();
  let intervalValue = data[key];
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      // 添加target到Dep
      dep.depend();
      return intervalValue;
    },
    set: (value) => {
      intervalValue = value;
      // 重新运行存储的方法
      dep.notify();
    },
  });
});

再定义一个watcher,触发依赖收集

function watcher(func) {
  target = func;
  target(); // 执行回调函数
  target = null;
}

watcher(() => {
  data.total = data.price * data.quantity;
});

测试一下结果

console.log('total = ' + data.total); // 20
data.price = 20;
console.log('total = ' + data.total); // 40
data.quantity = 10;
console.log('total = ' + data.total); // 200

使用Proxy实现一个响应式系统

先来了解一下Proxy如何来使用。

const observedData = new Proxy(data, {
  get(target, key) {
    // 此处添加依赖
  },
  set(target, key, value) {
    // 通知订阅者
  },
  deleteProperty(target, key) {
    // 删除属性
  }
});

set()方法中,target指的是就是被代理的那个对象,也就是datakey就是当前被访问属性的key。那么value就是被setter的时候传入的那个值。set方法的神奇之处就是它能感知到属性的添加。

给对象设置deleteProperty就能拦截到某个属性被删除的操作。结合set()deleteProperty(),完全可以将Vue.set()方法给抛弃掉。

实现响应式系统

首先,修改之前写的Object.keys(data).forEach循环,为每个响应式属性添加一个new Dep

// 存储在一个Map中
const deps = new Map();

Object.keys(data).forEach((key) => {
  // 每个属性设置一个Dep实例
  deps.set(key, new Dep());
});

接着给data设置代理

// 存储源数据
let data_without_proxy = data;

data = new Proxy(data_without_proxy, {
  get(target, key) {
    deps.get(key).depend(); // 添加到依赖
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    deps.get(key).notify(); // 重新运行存储的方法
  },
  deleteProperty(target, key) {
    Reflect.deleteProperty(target, key); // 使用Reflect API删除属性
    deps.get(key).notify(); // 重新运行存储的方法
  },
});

修改一下原始数据和测试数据

// 原始数据
let data = {
  price: 10,
  quantity: 2,
  discount: 5, // 新增一个discount
};

...

let total;
watcher(() => {
  total = (data.price - (data.discount || 0)) * data.quantity;
});

console.log('total = ' + total); // 10
data.price = 20;
console.log('total = ' + total); // 30
data.quantity = 10;
console.log('total = ' + total); // 150

Reflect.deleteProperty(data, 'discount');
// 删除掉discount属性,total立即变为了200
console.log('total = ' + total); // 200

再来试试添加一个属性

deps.set('discount', new Dep()); // 给属性设置一个新的依赖实例
data['discount'] = 5; // 添加属性
let salePrice;
watcher(() => {
  salePrice = data.price - data.discount;
});

console.log('salePrice = ' + salePrice); // 15
data.discount = 7.5;
console.log('salePrice = ' + salePrice); // 12.5

salePrice被自动更新了。

到此使用Proxy实现一个简易的响应式系统就完成了。