MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue 2与Vue 3 响应式系统从defineProperty到Proxy的演进

2021-11-236.6k 阅读

Vue 2 响应式系统:Object.defineProperty 的实现原理与局限

在 Vue 2 中,响应式系统的核心是 Object.defineProperty 方法。这个方法在 ES5 中被引入,允许开发者精确地控制对象属性的行为,包括读取、写入、枚举和删除等操作。Vue 2 利用 Object.defineProperty 来实现数据劫持,从而追踪数据的变化并触发视图更新。

1. 基本原理

Object.defineProperty 方法接受三个参数:目标对象、属性名和描述符对象。描述符对象可以定义属性的各种特性,如 value(属性值)、writable(是否可写)、enumerable(是否可枚举)和 configurable(是否可配置)。更重要的是,它还可以定义 gettersetter 函数。

以下是一个简单的示例,展示如何使用 Object.defineProperty 来创建一个具有 gettersetter 的属性:

let person = {};
let name = 'John';

Object.defineProperty(person, 'name', {
  get() {
    return name;
  },
  set(newValue) {
    name = newValue;
    console.log('Name has been updated to:', newValue);
  }
});

console.log(person.name); // 输出: John
person.name = 'Jane'; // 输出: Name has been updated to: Jane

在 Vue 2 中,当一个对象被传入 Vue 实例作为数据时,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty 将这些属性转换为 gettersetter。这样,当数据被读取(触发 getter)或修改(触发 setter)时,Vue 就能追踪到这些变化。

2. Vue 2 中的实现

Vue 2 的响应式系统主要包含两个核心部分:ObserverWatcher

Observer:负责将一个普通对象转换为响应式对象。它通过递归调用 Object.defineProperty 为对象的每个属性添加 gettersetter

function Observer(data) {
  if (!data || typeof data!== 'object') {
    return;
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key]);
  });
}

function defineReactive(obj, key, val) {
  Observer(val); // 递归处理子对象
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      console.log(`${key} has been updated to: ${newVal}`);
      // 这里应该触发依赖更新,实际 Vue 2 中有更复杂的实现
    }
  });
}

let data = {
  message: 'Hello, Vue 2!'
};
Observer(data);
console.log(data.message); // 输出: Hello, Vue 2!
data.message = 'Hello, new world!'; // 输出: message has been updated to: Hello, new world!

Watcher:负责监听数据的变化,并在数据变化时触发视图更新。当 getter 被触发时,Watcher 会将自己添加到依赖列表中。当 setter 被触发时,Watcher 会收到通知并执行更新操作。

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn);
    this.value = this.get();
  }

  get() {
    Dep.target = this;
    let value = this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
  }

  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

function parsePath(path) {
  if (typeof path ==='string') {
    let segments = path.split('.');
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
        if (!obj) return;
        obj = obj[segments[i]];
      }
      return obj;
    };
  }
  return path;
}

// Dep 是依赖收集器
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  removeSub(sub) {
    remove(this.subs, sub);
  }

  depend() {
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

// 为属性添加 Dep 实例
function defineReactive(obj, key, val) {
  let dep = new Dep();
  Observer(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      dep.notify();
    }
  });
}

3. 局限

尽管 Object.defineProperty 为 Vue 2 的响应式系统提供了基础,但它存在一些局限性:

  • 无法监听数组变化:虽然 Vue 2 对数组的部分方法(如 pushpopshiftunshiftsplicesortreverse)进行了包裹以触发视图更新,但对于直接通过索引修改数组元素或修改数组长度的操作,Object.defineProperty 无法监听到变化。例如:
let arr = [1, 2, 3];
Observer(arr);
arr[0] = 4; // 不会触发 set 操作
arr.length = 2; // 不会触发 set 操作
  • 对象新增属性无法监听:当在运行时给对象新增属性时,由于没有使用 Object.defineProperty 对新属性进行初始化,Vue 2 无法自动将其转换为响应式属性。例如:
let obj = { name: 'John' };
Observer(obj);
obj.age = 30; // 不会触发 set 操作,视图不会更新
  • 性能问题:在处理大型对象时,递归遍历对象并使用 Object.defineProperty 为每个属性添加 gettersetter 会带来一定的性能开销。而且,当数据变化频繁时,依赖收集和更新的过程也可能导致性能问题。

Vue 3 响应式系统:Proxy 的优势与实现

Vue 3 引入了 Proxy 来替代 Object.defineProperty 构建响应式系统。Proxy 是 ES6 中新增的特性,它允许开发者创建一个代理对象,用于拦截和自定义基本的操作,如属性访问、赋值、枚举、函数调用等。

1. Proxy 的基本用法

Proxy 构造函数接受两个参数:目标对象和处理程序对象。处理程序对象包含一系列的捕获器(trap)函数,用于定义代理对象的行为。

以下是一个简单的示例,展示如何使用 Proxy 来拦截对象属性的读取和写入:

let person = {
  name: 'John'
};

let proxy = new Proxy(person, {
  get(target, property) {
    console.log(`Getting property ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Setting property ${property} to ${value}`);
    target[property] = value;
    return true;
  }
});

console.log(proxy.name); // 输出: Getting property name, John
proxy.name = 'Jane'; // 输出: Setting property name to Jane

2. Vue 3 中的实现

在 Vue 3 中,响应式系统基于 Proxy 进行了重新设计。主要包括 reactive 函数用于创建响应式对象,以及 effect 函数用于注册副作用(如视图更新)。

function reactive(target) {
  return new Proxy(target, {
    get(target, property) {
      if (property === '__isReactive') {
        return true;
      }
      track(target, property);
      return Reflect.get(target, property);
    },
    set(target, property, value) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value);
      if (oldValue!== value) {
        trigger(target, property);
      }
      return result;
    }
  });
}

const targetMap = new WeakMap();

function track(target, property) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(property);
  if (!dep) {
    depsMap.set(property, (dep = new Set()));
  }
  if (activeEffect) {
    dep.add(activeEffect);
  }
}

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

let activeEffect;

function effect(fn) {
  const effect = function reactiveEffect() {
    activeEffect = effect;
    fn();
    activeEffect = null;
  };
  effect();
  return effect;
}

let state = reactive({
  message: 'Hello, Vue 3!'
});

effect(() => {
  console.log(state.message);
});

state.message = 'Hello, new world!';

在上述代码中:

  • reactive 函数使用 Proxy 创建了一个响应式代理对象,通过 get 捕获器进行依赖收集(track 函数),通过 set 捕获器触发依赖更新(trigger 函数)。
  • targetMap 是一个 WeakMap,用于存储每个目标对象的依赖关系。每个目标对象对应一个 Map,这个 Map 的键是属性名,值是一个 Set,包含依赖该属性的所有副作用函数。
  • effect 函数用于注册副作用函数。当副作用函数执行时,会激活 activeEffect,从而在 track 函数中收集依赖。当数据变化时,trigger 函数会遍历依赖的副作用函数并执行它们。

3. 优势

相比 Vue 2 的 Object.defineProperty 实现,Vue 3 使用 Proxy 带来了以下显著优势:

  • 原生支持数组监听Proxy 可以直接监听到数组的所有变化,包括通过索引修改元素和修改数组长度等操作。例如:
let arr = reactive([1, 2, 3]);

effect(() => {
  console.log(arr[0]);
});

arr[0] = 4; // 会触发依赖更新,输出新值 4
arr.length = 2; // 会触发依赖更新
  • 动态新增属性的响应式支持:使用 Proxy,在运行时给对象新增属性也能自动成为响应式属性。例如:
let obj = reactive({ name: 'John' });

effect(() => {
  console.log(obj.age);
});

obj.age = 30; // 会触发依赖更新,输出新值 30
  • 性能优化Proxy 是基于对象的代理,而不是像 Object.defineProperty 那样为每个属性单独设置 gettersetter。这使得在处理大型对象时,性能开销更小。而且,Vue 3 的依赖收集和更新机制更加高效,减少了不必要的更新。

响应式系统演进中的其他变化

除了从 Object.definePropertyProxy 的核心变化外,Vue 3 的响应式系统在其他方面也有一些重要的演进。

1. 更细粒度的依赖跟踪

在 Vue 2 中,依赖跟踪是基于对象的属性级别。而在 Vue 3 中,依赖跟踪更加细粒度,可以精确到具体的操作。例如,在 Vue 3 中,对于一个对象的不同属性访问,可以分别跟踪依赖。

let state = reactive({
  user: {
    name: 'John',
    age: 30
  }
});

effect(() => {
  console.log(state.user.name);
});

effect(() => {
  console.log(state.user.age);
});

state.user.name = 'Jane'; // 只会触发第一个 effect 的更新
state.user.age = 31; // 只会触发第二个 effect 的更新

2. 更好的 TypeScript 支持

Vue 3 对 TypeScript 的支持有了显著提升。Proxy 的类型声明更加清晰和准确,使得在使用 TypeScript 开发 Vue 3 应用时,代码的类型检查更加严格和可靠。例如,在定义响应式对象时,TypeScript 可以准确推断出对象的类型:

import { reactive } from 'vue';

interface User {
  name: string;
  age: number;
}

let user = reactive<User>({
  name: 'John',
  age: 30
});

// TypeScript 可以正确推断 user 的类型
user.name = 'Jane';
user.age = 31;

3. 与 Composition API 的紧密结合

Vue 3 的 Composition API 与新的响应式系统紧密结合,提供了更灵活和强大的方式来组织和复用代码。reactiveeffect 等响应式 API 是 Composition API 的基础,使得开发者可以在函数式组件中轻松地管理状态和副作用。

<template>
  <div>
    <p>{{ state.message }}</p>
    <button @click="updateMessage">Update Message</button>
  </div>
</template>

<script setup>
import { reactive, effect } from 'vue';

let state = reactive({
  message: 'Hello, Composition API!'
});

effect(() => {
  console.log('Message has been updated:', state.message);
});

function updateMessage() {
  state.message = 'Hello, new message!';
}
</script>

迁移与兼容性考虑

对于从 Vue 2 迁移到 Vue 3 的开发者,需要注意以下几点:

1. 语法变化

Vue 3 的响应式 API 有一些语法上的变化。例如,在 Vue 2 中使用 Vue.observable 创建响应式对象,而在 Vue 3 中使用 reactive 函数。此外,computedwatch 等 API 的使用方式也有一些调整。

2. 兼容性处理

虽然 Vue 3 提供了更好的性能和功能,但在一些旧环境中,可能需要考虑兼容性问题。例如,IE 浏览器不支持 Proxy,因此在需要兼容 IE 的项目中,可能需要使用一些 polyfill 或者继续使用 Vue 2。

3. 代码重构

迁移到 Vue 3 可能需要对部分代码进行重构,尤其是涉及到响应式系统的部分。开发者需要理解 Vue 3 新的响应式原理和 API,将旧代码适配到新的模式下。例如,处理数组和对象变化的方式需要根据新的响应式机制进行调整。

总结

Vue 从 2 到 3 的响应式系统演进,从 Object.definePropertyProxy 的转变,不仅解决了 Vue 2 中响应式系统的局限性,还带来了性能提升、更细粒度的依赖跟踪、更好的 TypeScript 支持以及与 Composition API 的紧密结合。了解这些变化对于开发者来说至关重要,无论是开发新的 Vue 3 项目,还是将现有的 Vue 2 项目迁移到 Vue 3,都能帮助开发者更好地利用 Vue 的新特性,构建更高效、更灵活的前端应用。