简单手写vue3的响应式原理

92 阅读4分钟

前言

该篇文章涉及到Proxy、defineProperty、Reflect的使用,详细使用方法和介绍可查看我上篇文章((深入JavaScript 八)defineProperty、Proxy、Reflect使用详细 - 掘金 (juejin.cn))。

该篇文章会先介绍响应式数据是什么,然后再我们手写vue3中实现创建响应式对象的reactive函数,和ref函数。

什么是响应式

我们在用过vue框架时会知道,在我们通过vue创建响应式数据后,当我们修改该响应式数据时,我们的代码会自动去执行与该响应式数据相关的代码块或者函数。所以我们设计的创建响应式函数应该具备以下几点:

  1. 自动收集与响应式数据相关的响应式函数,因为在我们的代码中有很多函数,而每个函数不一定肯定与我们响应式对象相关,如下,foo函数与obj对象相关,但bar函数与obj函数就不相关,所以收集的时候我们就只需要将foo函数收集起来。但可能我们可能只是单纯的使用一次obj.name,不想让所有函数随着obj.name的改变都重新执行,所以我们应该还需要一个watchFn函数来将我们的普通函数转换为我们需要的响应式函数。
let obj = {
    name:"aaa",
    age:12
}

function foo(){
    console.log(obj.name)
}

function bar(){
    console.log("bar")
}

2. 收集的函数需要与对象本身或者对象属性一一对应。就像上面代码一样,我们在foo函数中需要访问obj中的name属性,但是与age属性无关,无论age属性删除或者修改了,都不影响我们foo函数执行后的效果,所以我们收集的函数需要准确的与它们里面需要的响应式数据做一一对应。 3. 当对响应式数据进行操作的时候,需要通知收集的函数重新触发以达到刷新数据的效果。 4. 需要一个Depend类来自动帮我们管理这些函数的保存、执行

简单手写vue3中的reactive方法

结合上面我们提到的几点我们写出下列代码:

function isObject(obj) {
  return typeof obj === "object" && obj !== null;
}

function isFunction(fn) {
  return typeof fn === "function";
}

//缓存当前执行的需要进行响应式的函数
let reactiveFn = null;

//定义一个Depend类来收集和自动调用响应式函数
class Depend {
  constructor() {
    //使用Set收集的原因是避免属性值多次使用后重复收集函数
    this.fns = new Set();
  }

  addDepend() {
    if (reactiveFn) this.fns.add(reactiveFn);
  }

  notify() {
    for (const fn of this.fns) {
      fn();
    }
  }
}

//创建WeakMap类型数据而不是Map数据是因为可能在以后操作中我们会销毁obj对象
//Map对obj对象是强引用,会造成内存泄漏。所以这里用的是WeakMap
const dependWeakMap = new WeakMap();
//对于对象的每个属性都创建一个与该属性有关的Depend类,来将收集到的响应式函数按照对象属性的key进行分类。
//这个函数的作用就是初始化Depend与对象的映射关系,并且按照对象和属性的不同返回对应的Depend对象
function getDepend(obj, key) {
  let targetMap = dependWeakMap.get(obj);
  if (!targetMap) {
    targetMap = new Map();
    dependWeakMap.set(obj, targetMap);
  }
  let targetDepend = targetMap.get(key);
  if (!targetDepend) {
    targetDepend = new Depend();
    targetMap.set(key, targetDepend);
  }

  return targetDepend;
}
//将函数转换为响应式函数
function watchFn(fn) {
  if (!isFunction(fn)) {
    throw new Error("fn is not a function");
  }
  reactiveFn = fn;
  fn();
  reactiveFn = null;
}

//将传入的对象转换成代理对象
function reactive(obj) {
  if (!isObject(obj)) {
    throw new Error("fn is not a object");
  }
  return new Proxy(obj, {
    get(target, key, receiver) {
      const depend = getDepend(target, key);
      //收集响应式函数
      depend.addDepend();
      const value = Reflect.get(target, key, receiver);
      //如果根据key获取的value是一个对象,那么返回的时候也应该把这个对象转换为响应式对象
      if (isObject(value)) {
        return reactive(value);
      }
      return value;
    },
    set(target, key, newValue, receiver) {
      const value = Reflect.get(target, key, receiver);
      //set触发器应该返回一个布尔值来表示设置值成功没有,因为在严格模式下,如果返回false那么会抛出一个TypeError异常
      let isChanged = true;
      //判断修改值是否与旧值相等,节省性能
      if (value !== newValue) {
        isChanged = Reflect.set(target, key, newValue, receiver);
        const depend = getDepend(target, key);
        //自动调用收集到的响应式函数
        depend.notify();
      }

      return isChanged;
    },
  });
}

使用方法:

const objProxy = reactive({
  name: "aaa",
  obj: {
    name: "bbb",
  },
});

watchFn(() => {
  console.log("name:", objProxy.name);
});

objProxy.name = "bbb";
objProxy.age = 10;

watchFn(() => {
  console.log("age:", objProxy.age);
});

watchFn(() => {
  console.log("obj.name:", objProxy.obj.name);
});

objProxy.obj.name = "bbb2";

代码运行后控制台打印:

image.png

我们可以看到,当我们修改name或者新增age属性的时候,我们自动收集到的响应式函数都会获取到最新的值并自动调用。

Vue3中的ref方法

我们在使用vue3中ref的时候会发现,每次都是使用的xxxx.value来获取值或者修改值。这相当于就是reactive({value:"xxx"}),所以在这里我们就借用上面我们实现的reactive来实现:

function ref(data) {
  //如果传入的是对象就直接调用reactive
  if (isObject(data)) return reactive(data);
  
  let obj = {
    value: data,
  };
  //不是对象我们就自己建一个对象obj
  return reactive(obj);
}

使用:

const data = ref("123");
watchFn(() => {
  console.log("value:", data.value);
});

data.value = 111;

image.png

打印出来没问题。