Skip to content

响应式原理

Vue的响应式系统是其核心特性之一,它使得数据与视图之间能够自动同步,当数据发生变化时,视图会自动更新,无需手动操作DOM。本文将详细介绍Vue的响应式原理,包括Vue 2和Vue 3的实现方式。

Vue 2的响应式原理

1. Object.defineProperty

Vue 2的响应式系统是基于 Object.defineProperty 实现的,它通过拦截对象的 getset 方法来实现数据的响应式。

1.1 基本原理

javascript
// 简化版的响应式实现
function defineReactive(obj, key, value) {
  // 递归处理嵌套对象
  if (typeof value === 'object' && value !== null) {
    observe(value);
  }
  
  // 定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`获取属性 ${key}: ${value}`);
      // 依赖收集
      dep.depend();
      return value;
    },
    set(newValue) {
      if (value === newValue) {
        return;
      }
      console.log(`设置属性 ${key}: ${newValue}`);
      value = newValue;
      // 递归处理新值
      if (typeof newValue === 'object' && newValue !== null) {
        observe(newValue);
      }
      // 触发更新
      dep.notify();
    }
  });
}

// 观察对象
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }
  
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// 测试
const obj = {
  name: 'John',
  age: 30,
  address: {
    street: '123 Main St',
    city: 'New York'
  }
};

observe(obj);

// 触发get
console.log(obj.name); // 获取属性 name: John

// 触发set
obj.age = 31; // 设置属性 age: 31

// 触发嵌套对象的get
console.log(obj.address.street); // 获取属性 street: 123 Main St

// 触发嵌套对象的set
obj.address.city = 'London'; // 设置属性 city: London

1.2 依赖收集与触发更新

Vue 2的响应式系统通过以下步骤实现依赖收集和触发更新:

  1. 依赖收集:当组件渲染时,会访问组件中使用的数据属性,触发这些属性的 get 方法。在 get 方法中,会将当前的渲染Watcher添加到该属性的依赖列表中。

  2. 触发更新:当数据属性发生变化时,会触发该属性的 set 方法。在 set 方法中,会通知该属性的依赖列表中的所有Watcher进行更新。

1.3 依赖收集的实现

javascript
// 简化版的Dep类
class Dep {
  constructor() {
    this.subscribers = [];
  }
  
  // 添加依赖
  depend() {
    if (Dep.target) {
      this.subscribers.push(Dep.target);
    }
  }
  
  // 通知更新
  notify() {
    this.subscribers.forEach(subscriber => {
      subscriber.update();
    });
  }
}

// 简化版的Watcher类
class Watcher {
  constructor(fn) {
    this.fn = fn;
    // 将当前Watcher设置为Dep.target
    Dep.target = this;
    // 执行fn,触发依赖收集
    this.fn();
    // 重置Dep.target
    Dep.target = null;
  }
  
  // 更新
  update() {
    this.fn();
  }
}

// 全局Dep.target
Dep.target = null;

// 测试
const obj = {
  name: 'John',
  age: 30
};

// 观察对象
observe(obj);

// 创建Watcher
new Watcher(() => {
  console.log(`渲染:${obj.name}, ${obj.age}`);
});

// 触发更新
obj.name = 'Jane'; // 渲染:Jane, 30
obj.age = 31; // 渲染:Jane, 31

2. 局限性

Vue 2的响应式系统基于 Object.defineProperty 实现,存在以下局限性:

  1. 无法检测对象属性的添加和删除Object.defineProperty 只能拦截已存在的属性的 getset 方法,无法检测对象属性的添加和删除。

  2. 无法检测数组的变化Object.defineProperty 无法检测数组的变化,Vue 2通过重写数组的方法来解决这个问题。

  3. 性能问题:当对象的属性较多时,Object.defineProperty 需要遍历所有属性并为每个属性定义getter和setter,可能会影响性能。

Vue 3的响应式原理

1. Proxy

Vue 3的响应式系统是基于 Proxy 实现的,它通过拦截对象的操作来实现数据的响应式。Proxy 是ES6引入的特性,它可以拦截对象的多种操作,包括属性的读取、设置、添加、删除等。

1.1 基本原理

javascript
// 简化版的响应式实现
function reactive(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  return new Proxy(obj, {
    // 拦截属性读取
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      console.log(`获取属性 ${key}: ${result}`);
      // 依赖收集
      track(target, key);
      // 递归处理嵌套对象
      return typeof result === 'object' && result !== null ? reactive(result) : result;
    },
    
    // 拦截属性设置
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        console.log(`设置属性 ${key}: ${value}`);
        // 触发更新
        trigger(target, key);
      }
      return result;
    },
    
    // 拦截属性删除
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      console.log(`删除属性 ${key}`);
      // 触发更新
      trigger(target, key);
      return result;
    },
    
    // 拦截属性枚举
    ownKeys(target) {
      console.log('枚举属性');
      return Reflect.ownKeys(target);
    }
  });
}

// 依赖收集
function track(target, key) {
  // 简化实现,实际会将当前的effect添加到依赖列表中
  console.log(`收集依赖:${key}`);
}

// 触发更新
function trigger(target, key) {
  // 简化实现,实际会通知依赖列表中的所有effect进行更新
  console.log(`触发更新:${key}`);
}

// 测试
const obj = {
  name: 'John',
  age: 30,
  address: {
    street: '123 Main St',
    city: 'New York'
  }
};

const reactiveObj = reactive(obj);

// 触发get
console.log(reactiveObj.name); // 获取属性 name: John

// 触发set
reactiveObj.age = 31; // 设置属性 age: 31

// 触发嵌套对象的get
console.log(reactiveObj.address.street); // 获取属性 street: 123 Main St

// 触发嵌套对象的set
reactiveObj.address.city = 'London'; // 设置属性 city: London

// 触发属性添加
reactiveObj.gender = 'male'; // 设置属性 gender: male

// 触发属性删除
delete reactiveObj.gender; // 删除属性 gender

1.2 依赖收集与触发更新

Vue 3的响应式系统通过以下步骤实现依赖收集和触发更新:

  1. 依赖收集:当effect执行时,会访问effect中使用的数据属性,触发这些属性的 get 方法。在 get 方法中,会将当前的effect添加到该属性的依赖列表中。

  2. 触发更新:当数据属性发生变化时,会触发该属性的 set 方法。在 set 方法中,会通知该属性的依赖列表中的所有effect进行更新。

1.3 依赖收集的实现

javascript
// 简化版的effect实现
let activeEffect = null;

function effect(fn) {
  const effectFn = () => {
    // 清除之前的依赖
    cleanup(effectFn);
    // 将当前effect设置为activeEffect
    activeEffect = effectFn;
    // 执行fn,触发依赖收集
    fn();
    // 重置activeEffect
    activeEffect = null;
  };
  
  // 存储依赖的集合
  effectFn.deps = [];
  
  // 执行effect
  effectFn();
}

// 清除依赖
function cleanup(effectFn) {
  effectFn.deps.forEach(dep => {
    dep.delete(effectFn);
  });
  effectFn.deps.length = 0;
}

// 存储依赖的WeakMap
const targetMap = new WeakMap();

// 依赖收集
function track(target, key) {
  if (!activeEffect) return;
  
  // 获取target的依赖Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map());
  }
  
  // 获取key的依赖Set
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, dep = new Set());
  }
  
  // 添加依赖
  dep.add(activeEffect);
  // 存储依赖到effect的deps中
  activeEffect.deps.push(dep);
}

// 触发更新
function trigger(target, key) {
  // 获取target的依赖Map
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  // 获取key的依赖Set
  const dep = depsMap.get(key);
  if (!dep) return;
  
  // 通知所有依赖进行更新
  const effectsToRun = new Set(dep);
  effectsToRun.forEach(effect => {
    effect();
  });
}

// 测试
const obj = {
  name: 'John',
  age: 30
};

const reactiveObj = reactive(obj);

// 创建effect
effect(() => {
  console.log(`渲染:${reactiveObj.name}, ${reactiveObj.age}`);
});

// 触发更新
reactiveObj.name = 'Jane'; // 渲染:Jane, 30
reactiveObj.age = 31; // 渲染:Jane, 31

// 触发属性添加
reactiveObj.gender = 'male'; // 渲染:Jane, 31, male

// 触发属性删除
delete reactiveObj.gender; // 渲染:Jane, 31

2. 优势

Vue 3的响应式系统基于 Proxy 实现,相比Vue 2的 Object.defineProperty 有以下优势:

  1. 支持对象属性的添加和删除Proxy 可以拦截对象属性的添加和删除操作,因此Vue 3可以检测到对象属性的添加和删除。

  2. 支持数组的所有操作Proxy 可以拦截数组的所有操作,包括 pushpopshiftunshiftsplicesortreverse 等,因此Vue 3可以检测到数组的所有变化。

  3. 性能更好Proxy 是ES6原生支持的特性,相比 Object.defineProperty,它的性能更好,特别是在处理大型对象时。

  4. 代码更简洁Proxy 可以拦截对象的多种操作,代码更简洁,维护成本更低。

Vue 3的响应式API

Vue 3提供了以下响应式API:

1. reactive

reactive 函数用于创建一个响应式对象,它接受一个普通对象作为参数,返回一个响应式代理对象。

javascript
import { reactive } from 'vue';

const state = reactive({
  count: 0,
  name: 'John'
});

// 访问属性
console.log(state.count); // 0

// 修改属性
state.count++; // 1
console.log(state.count); // 1

2. ref

ref 函数用于创建一个响应式的引用类型,它接受一个普通值作为参数,返回一个响应式的ref对象。

javascript
import { ref } from 'vue';

const count = ref(0);
const name = ref('John');

// 访问值
console.log(count.value); // 0

// 修改值
count.value++;
console.log(count.value); // 1

// 在模板中使用
// <template>
//   <div>{{ count }}</div>
//   <button @click="count++">Increment</button>
// </template>

3. computed

computed 函数用于创建一个计算属性,它接受一个计算函数作为参数,返回一个响应式的计算属性对象。

javascript
import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() => count.value * 2);

// 访问计算属性
console.log(doubleCount.value); // 0

// 修改依赖的值
count.value++;
console.log(doubleCount.value); // 2

4. watch

watch 函数用于监听数据的变化,它接受三个参数:监听的数据源、回调函数和配置选项。

javascript
import { ref, watch } from 'vue';

const count = ref(0);

// 监听单个数据源
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`);
});

// 修改值
count.value++;
// 输出:count changed from 0 to 1

// 监听多个数据源
const name = ref('John');

watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`count changed from ${oldCount} to ${newCount}`);
  console.log(`name changed from ${oldName} to ${newName}`);
});

// 修改值
count.value++;
name.value = 'Jane';
// 输出:count changed from 1 to 2
// 输出:name changed from John to Jane

5. watchEffect

watchEffect 函数用于创建一个副作用函数,它会自动追踪函数中使用的响应式数据,当这些数据发生变化时,副作用函数会重新执行。

javascript
import { ref, watchEffect } from 'vue';

const count = ref(0);
const name = ref('John');

// 创建副作用函数
watchEffect(() => {
  console.log(`count: ${count.value}, name: ${name.value}`);
});
// 输出:count: 0, name: John

// 修改值
count.value++;
// 输出:count: 1, name: John

name.value = 'Jane';
// 输出:count: 1, name: Jane

响应式系统的应用

1. 组件数据

在Vue组件中,我们可以使用响应式数据来驱动组件的渲染:

vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const message = ref('Hello Vue!');
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    return {
      message,
      count,
      increment
    };
  }
};
</script>

2. 状态管理

在Vue中,我们可以使用响应式系统来实现状态管理,例如使用Pinia:

javascript
// store/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'John'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  }
});

// 组件中使用
import { useCounterStore } from '@/store/counter';

export default {
  setup() {
    const store = useCounterStore();

    return {
      store
    };
  }
};

3. 自定义响应式数据

我们可以使用Vue的响应式API来创建自定义的响应式数据:

javascript
import { reactive, ref, computed } from 'vue';

// 创建响应式对象
const user = reactive({
  name: 'John',
  age: 30
});

// 创建响应式引用
const loading = ref(false);

// 创建计算属性
const isAdult = computed(() => user.age >= 18);

// 监听数据变化
watch([() => user.age, loading], ([newAge, newLoading], [oldAge, oldLoading]) => {
  console.log(`age changed from ${oldAge} to ${newAge}`);
  console.log(`loading changed from ${oldLoading} to ${newLoading}`);
});

// 修改数据
user.age++;
loading.value = true;

响应式系统的注意事项

1. 响应式对象的局限性

  • Vue 2:无法检测对象属性的添加和删除,无法检测数组的变化(需要使用变异方法)。
  • Vue 3:可以检测对象属性的添加和删除,可以检测数组的所有变化。

2. 响应式数据的引用

  • reactive:返回的是一个响应式代理对象,直接修改对象的属性会触发更新。
  • ref:返回的是一个响应式的ref对象,需要通过 .value 访问和修改值。

3. 计算属性的缓存

计算属性会缓存其计算结果,只有当依赖的数据发生变化时,才会重新计算。这可以提高性能,特别是在计算复杂值时。

4. 监听的深度

  • watch:默认情况下,watch 只监听数据源的顶层属性,需要使用 deep: true 来开启深度监听。
  • watchEffect:默认情况下,watchEffect 会自动追踪函数中使用的所有响应式数据,包括嵌套属性。

5. 响应式系统的性能

  • 避免频繁修改响应式数据:频繁修改响应式数据会触发频繁的更新,影响性能。
  • 使用计算属性:对于复杂的计算,使用计算属性可以缓存计算结果,提高性能。
  • 使用watch的immediate选项:对于需要立即执行的监听,使用 immediate: true 可以避免手动触发。

面试常见问题

1. Vue的响应式原理是什么?

Vue的响应式系统是其核心特性之一,它使得数据与视图之间能够自动同步,当数据发生变化时,视图会自动更新,无需手动操作DOM。

  • Vue 2:基于 Object.defineProperty 实现,通过拦截对象的 getset 方法来实现数据的响应式。
  • Vue 3:基于 Proxy 实现,通过拦截对象的多种操作来实现数据的响应式。

2. Vue 2和Vue 3的响应式系统有什么区别?

Vue 2和Vue 3的响应式系统的区别主要包括:

特性Vue 2Vue 3
实现方式Object.definePropertyProxy
对象属性添加/删除无法检测可以检测
数组变化只能检测变异方法可以检测所有操作
性能较差,特别是大型对象较好
代码复杂度较高较低

3. Object.defineProperty的局限性是什么?

Object.defineProperty 的局限性主要包括:

  • 无法检测对象属性的添加和删除:只能拦截已存在的属性的 getset 方法。
  • 无法检测数组的变化:无法检测数组的 pushpopshiftunshiftsplicesortreverse 等操作。
  • 性能问题:当对象的属性较多时,需要遍历所有属性并为每个属性定义getter和setter,可能会影响性能。

4. Proxy的优势是什么?

Proxy 的优势主要包括:

  • 支持对象属性的添加和删除:可以拦截对象的 setdeleteProperty 操作。
  • 支持数组的所有操作:可以拦截数组的所有操作,包括 pushpopshiftunshiftsplicesortreverse 等。
  • 性能更好:是ES6原生支持的特性,性能更好,特别是在处理大型对象时。
  • 代码更简洁:可以拦截对象的多种操作,代码更简洁,维护成本更低。

5. Vue 3的响应式API有哪些?

Vue 3的响应式API主要包括:

  • reactive:创建一个响应式对象。
  • ref:创建一个响应式的引用类型。
  • computed:创建一个计算属性。
  • watch:监听数据的变化。
  • watchEffect:创建一个副作用函数,自动追踪依赖。

6. reactive和ref的区别是什么?

reactiveref 的区别主要包括:

特性reactiveref
输入类型普通对象普通值
返回类型响应式代理对象响应式ref对象
访问方式直接访问属性通过 .value 访问值
修改方式直接修改属性通过 .value 修改值
适用场景复杂对象简单值

7. computed和watch的区别是什么?

computedwatch 的区别主要包括:

特性computedwatch
用途计算属性,用于派生状态监听数据变化,执行副作用
缓存有缓存,依赖不变时不会重新计算无缓存,依赖变化时会重新执行
同步/异步同步执行可以异步执行
立即执行首次访问时执行默认首次不执行,需要使用 immediate: true
参数计算函数监听数据源、回调函数、配置选项

8. 如何实现深度响应式?

  • Vue 2:通过递归遍历对象的所有属性,为每个属性定义 getset 方法。
  • Vue 3:通过 Proxyget 陷阱,当访问嵌套对象时,递归创建响应式代理对象。

9. 响应式系统的依赖收集是如何实现的?

响应式系统的依赖收集主要通过以下步骤实现:

  1. 依赖收集:当组件渲染或effect执行时,会访问响应式数据的属性,触发属性的 get 方法。在 get 方法中,会将当前的渲染Watcher或effect添加到该属性的依赖列表中。

  2. 触发更新:当响应式数据的属性发生变化时,会触发属性的 set 方法。在 set 方法中,会通知该属性的依赖列表中的所有Watcher或effect进行更新。

10. 如何避免响应式系统的性能问题?

避免响应式系统的性能问题的方法主要包括:

  • 避免频繁修改响应式数据:频繁修改响应式数据会触发频繁的更新,影响性能。
  • 使用计算属性:对于复杂的计算,使用计算属性可以缓存计算结果,提高性能。
  • 使用watch的deep选项:对于需要深度监听的对象,使用 deep: true 来开启深度监听,但要注意性能影响。
  • 使用shallowReactive和shallowRef:对于不需要深度响应的对象,使用 shallowReactiveshallowRef 可以提高性能。
  • 使用readonly:对于不需要修改的对象,使用 readonly 可以提高性能,同时防止意外修改。

总结

Vue的响应式系统是其核心特性之一,它使得数据与视图之间能够自动同步,当数据发生变化时,视图会自动更新,无需手动操作DOM。

  • Vue 2:基于 Object.defineProperty 实现,通过拦截对象的 getset 方法来实现数据的响应式,存在无法检测对象属性的添加和删除、无法检测数组的所有变化等局限性。

  • Vue 3:基于 Proxy 实现,通过拦截对象的多种操作来实现数据的响应式,支持对象属性的添加和删除、支持数组的所有变化,性能更好,代码更简洁。

Vue 3提供了丰富的响应式API,包括 reactiverefcomputedwatchwatchEffect 等,这些API使得开发者可以更灵活地使用响应式系统。

通过理解Vue的响应式原理,开发者可以更好地使用Vue的响应式系统,避免常见的性能问题,开发出更高质量的Vue应用。

好好学习,天天向上