初探 Vue 3 响应式源码(一): Reactive

272 阅读3分钟

Vue的响应式一直是面试中的重点,大家也都知道Vue3的响应式是弃用了Vue2中使用的Object.defineProperty,而改用了ES6新增的 Proxy ,我们来看一下Vue3的响应式是怎么做的

tip:本文需要Proxy、Reflect以及Vue3中的Effect等前置知识,如果不熟悉先去看下文档哦


一、 实现 reactive 函数

通过reactive创建的对象是如何实现响应式的呢,核心就是利用 Proxy 拦截对象的读取和设置操作,并在这些操作中实现依赖收集和触发更新。

1.1 基本实现

我们先实现一个简单的 reactive 函数,让他能够支持在读取和写入的时候都被拦截到,方便我们做后续操作。

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
        console.log(`读取属性:${key}`);
        return Reflect.get(target, key, receiver); // 可暂时理解为 return target[key]
    },
    set(target, key, value, receiver) {
      console.log(`设置属性:${key} = ${value}`);
      return Reflect.set(target, key, value, receiver);  // 可暂时理解为 return target[key] = value)
    },
  };
  return new Proxy(target, handler); 
}

const obj = reactive({ name: '张三' });
obj.name; // 输出:读取属性:name
obj.name = '李四'; // 输出:设置属性:name = 李四

1.2 处理嵌套对象

如果对象的属性值也是一个对象,我们需要递归地将它转换为响应式对象。为此,可以在 get 拦截器中判断属性值是否为对象,如果是,则递归调用 reactive

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      console.log(`读取属性:${key}`);
      // 如果属性值是对象,则递归调用 reactive
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(target, key, value, receiver) {
      console.log(`设置属性:${key} = ${value}`);
      return Reflect.set(target, key, value, receiver);
    },
  };
  return new Proxy(target, handler);
}

const obj = reactive({ name: '张三', info: { age: 25 } });
obj.info.age; // 输出:读取属性:info -> 读取属性:age
obj.info.age = 26; // 输出:读取属性:info -> 设置属性:age = 26

二、 依赖收集和触发更新

前面我们已经可以拦截到值的读取和写入了,那用到obj.name的地方如何能在obj.name写入新值的时候能及时更新呢??

那就要说到“依赖收集”和“依赖更新”了,也就是说在用到obj.name的时候,我们对其进行统计,然后在obj.name被赋新值后,把刚才名单上的用到obj.name的某方法A带来的effect去执行

“好嘛,fn1fn2里都用到了obj.name,把他俩带过来的回调(effect)记到名单上,下次obj.name写入的时候,把obj.name上名单的effect们挨个执行!”

tip:effect指的是响应式系统中用于追踪依赖和触发更新的函数。

2.1 依赖收集

为了实现依赖收集,我们需要:

  1. 定义一个全局变量 activeEffect,用于存储当前正在执行的副作用函数。
  2. get 拦截器中,将 activeEffect 收集到依赖集合中。
let activeEffect = null;
const targetMap = new WeakMap(); // 存储目标对象及其依赖,这里使用WeakMap而不是Map是为了利用其弱引用特性,避免内存泄漏,并更符合依赖收集的语义。

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  // 哦?target这个对象还没有创建过依赖?创建!
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  // 啥?target.name这个name竟然没创建对应依赖?创建!
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect);
}

2.2 触发更新

set 拦截器中,我们需要从依赖集合中取出所有副作用函数并执行:

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach(effect => effect());
  }
}

2.3 整合到 reactive 中

tracktrigger 整合到 reactive 中:

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, key); // 收集依赖
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      const success = Reflect.set(target, key, value, receiver);
      // 判断是不是新值,不是新值就别白折腾了
      if (success && oldValue !== value) {
        trigger(target, key); // 触发更新
      }
      return success;
    },
  };
  return new Proxy(target, handler);
}

三、完成

我们来测试一下:

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

const obj = reactive({ name: '张三', info: { age: 25 } });

effect(() => {
  console.log(`名字:${obj.name}`);
});

effect(() => {
  console.log(`年龄:${obj.info.age}`);
});

obj.name = '李四'; // 输出:名字:李四
obj.info.age = 26; // 输出:年龄:26

没有问题,完成~