Skip to content
On this page

Vue.js 响应式系统

Vue.js 的响应式系统是其核心特性之一,它使得数据变化能够自动更新到视图层。理解响应式系统的工作原理对于深入使用Vue.js至关重要。

响应式原理

Vue 2 的响应式实现

在 Vue 2 中,响应式系统基于 Object.defineProperty() 实现:

javascript
// Vue 2 响应式原理示例
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  observe(val);
  
  const dep = new Dep(); // 依赖收集器
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      // 依赖收集
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 通知更新
      dep.notify();
    }
  });
}

function observe(value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  
  // 遍历对象的所有属性
  Object.keys(value).forEach(key => {
    defineReactive(value, key, value[key]);
  });
}

Vue 3 的响应式实现

Vue 3 使用 ProxyReflect 实现了更强大的响应式系统:

javascript
// Vue 3 响应式原理示例
function reactive(target) {
  if (!isObject(target)) {
    return target;
  }
  
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      
      // 只有值真正改变时才触发更新
      if (hasChanged(value, oldValue)) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      trigger(target, key);
      return result;
    }
  });
}

响应式 API

reactive()

reactive() 函数将一个对象转换为响应式对象:

vue
<template>
  <div>
    <p>计数器: {{ state.count }}</p>
    <p>双倍: {{ state.double }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { reactive, computed } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: 'Vue 3'
    });
    
    // 使用计算属性
    const double = computed(() => state.count * 2);
    
    // 添加到响应式对象中
    state.double = double;
    
    const increment = () => {
      state.count++;
    };
    
    return {
      state,
      increment
    };
  }
}
</script>

ref()

ref() 函数创建一个响应式引用:

vue
<template>
  <div>
    <p>计数器: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0);
    
    const doubleCount = computed(() => count.value * 2);
    
    const increment = () => {
      count.value++;
    };
    
    return {
      count,
      doubleCount,
      increment
    };
  }
}
</script>

toRefs()

toRefs() 函数将响应式对象转换为普通对象,其中每个属性都是指向原始对象相应属性的 ref:

vue
<template>
  <div>
    <p>姓名: {{ name }}</p>
    <p>年龄: {{ age }}</p>
    <button @click="updateUser">更新用户</button>
  </div>
</template>

<script>
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const user = reactive({
      name: 'John',
      age: 30
    });
    
    const updateUser = () => {
      user.name = 'Jane';
      user.age = 25;
    };
    
    // 将响应式对象转换为ref
    return {
      ...toRefs(user),
      updateUser
    };
  }
}
</script>

深层响应式 vs 浅层响应式

深层响应式 (deep reactive)

vue
<template>
  <div>
    <p>用户信息: {{ user.name }}, {{ user.profile.age }}</p>
    <button @click="updateProfile">更新资料</button>
  </div>
</template>

<script>
import { reactive } from 'vue'

export default {
  setup() {
    const user = reactive({
      name: 'John',
      profile: {
        age: 30,
        address: {
          city: 'New York'
        }
      }
    });
    
    const updateProfile = () => {
      // 嵌套对象的属性变化也会触发更新
      user.profile.age = 31;
      user.profile.address.city = 'Boston';
    };
    
    return {
      user,
      updateProfile
    };
  }
}
</script>

浅层响应式 (shallow reactive)

vue
<template>
  <div>
    <p>用户信息: {{ user.name }}</p>
    <p>资料年龄: {{ user.profile.age }}</p>
    <button @click="updateUser">更新用户</button>
    <button @click="updateProfile">更新资料</button>
  </div>
</template>

<script>
import { shallowReactive, reactive } from 'vue'

export default {
  setup() {
    const user = shallowReactive({
      name: 'John',
      profile: {
        age: 30
      }
    });
    
    const updateUser = () => {
      // 这会触发更新(直接属性)
      user.name = 'Jane';
    };
    
    const updateProfile = () => {
      // 这不会触发更新(嵌套对象属性)
      // 需要使用 reactive 包装 profile
      user.profile.age = 31;
    };
    
    return {
      user,
      updateUser,
      updateProfile
    };
  }
}
</script>

计算属性和侦听器

computed()

计算属性具有缓存特性,只有依赖变化时才会重新计算:

vue
<template>
  <div>
    <p>原始列表: {{ list.join(', ') }}</p>
    <p>过滤列表: {{ filteredList.join(', ') }}</p>
    <p>计算属性执行次数: {{ computedCount }}</p>
    <button @click="addItem">添加项目</button>
    <button @click="filterOdd">过滤奇数</button>
  </div>
</template>

<script>
import { ref, computed } from 'vue'

export default {
  setup() {
    const list = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    const shouldFilterOdd = ref(true);
    const computedCount = ref(0);
    
    const filteredList = computed(() => {
      computedCount.value++;
      console.log('计算属性执行');
      
      if (shouldFilterOdd.value) {
        return list.value.filter(item => item % 2 === 0);
      }
      return list.value;
    });
    
    const addItem = () => {
      list.value.push(list.value.length + 1);
    };
    
    const filterOdd = () => {
      shouldFilterOdd.value = !shouldFilterOdd.value;
    };
    
    return {
      list,
      filteredList,
      computedCount,
      addItem,
      filterOdd
    };
  }
}
</script>

watch() 和 watchEffect()

vue
<template>
  <div>
    <input v-model="query" placeholder="输入查询">
    <p>查询: {{ query }}</p>
    <p>延迟查询: {{ debouncedQuery }}</p>
    <p>搜索结果: {{ results }}</p>
  </div>
</template>

<script>
import { ref, watch, watchEffect } from 'vue'

export default {
  setup() {
    const query = ref('');
    const debouncedQuery = ref('');
    const results = ref([]);
    
    // watchEffect - 自动追踪依赖
    watchEffect(async () => {
      if (debouncedQuery.value) {
        // 模拟API调用
        results.value = await searchAPI(debouncedQuery.value);
      } else {
        results.value = [];
      }
    });
    
    // watch - 显式指定监听源
    let timeoutId;
    watch(query, (newQuery) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        debouncedQuery.value = newQuery;
      }, 300);
    });
    
    // 清理副作用
    onUnmounted(() => {
      clearTimeout(timeoutId);
    });
    
    return {
      query,
      debouncedQuery,
      results
    };
  }
}

// 模拟API调用
function searchAPI(query) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([`结果1 for ${query}`, `结果2 for ${query}`, `结果3 for ${query}`]);
    }, 100);
  });
}
</script>

响应式系统最佳实践

避免的问题

vue
<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }} - {{ item.completed ? '完成' : '未完成' }}
        <button @click="toggleItem(item)">切换</button>
      </li>
    </ul>
    <button @click="addItem">添加项目</button>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const items = ref([
      { id: 1, name: '项目1', completed: false },
      { id: 2, name: '项目2', completed: true }
    ]);
    
    // 正确的数组更新方法
    const toggleItem = (item) => {
      // 方法1: 使用索引更新
      const index = items.value.findIndex(i => i.id === item.id);
      if (index !== -1) {
        items.value[index].completed = !items.value[index].completed;
      }
      
      // 方法2: 使用解构创建新对象
      // items.value = items.value.map(i => 
      //   i.id === item.id ? { ...i, completed: !i.completed } : i
      // );
    };
    
    const addItem = () => {
      const newItem = {
        id: Date.now(), // 确保唯一ID
        name: `项目${items.value.length + 1}`,
        completed: false
      };
      items.value.push(newItem);
    };
    
    return {
      items,
      toggleItem,
      addItem
    };
  }
}
</script>

性能优化

vue
<template>
  <div>
    <input v-model="searchTerm" placeholder="搜索">
    <div v-for="item in displayedItems" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

<script>
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const allItems = ref(Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `项目${i}`,
      category: i % 3 === 0 ? 'A' : i % 3 === 1 ? 'B' : 'C'
    })));
    
    const searchTerm = ref('');
    
    // 使用计算属性进行过滤,利用缓存
    const filteredItems = computed(() => {
      if (!searchTerm.value) return allItems.value;
      return allItems.value.filter(item => 
        item.name.toLowerCase().includes(searchTerm.value.toLowerCase())
      );
    });
    
    // 分页显示,避免一次性渲染大量DOM
    const currentPage = ref(0);
    const pageSize = 50;
    
    const displayedItems = computed(() => {
      const start = currentPage.value * pageSize;
      return filteredItems.value.slice(start, start + pageSize);
    });
    
    // 监听搜索词变化,重置页面
    watch(searchTerm, () => {
      currentPage.value = 0;
    });
    
    return {
      searchTerm,
      displayedItems,
      filteredItems
    };
  }
}
</script>

响应式系统高级用法

自定义 ref

javascript
// 自定义防抖 ref
import { customRef } from 'vue'

export function useDebounce(value, delay = 200) {
  let timeout;
  return customRef((track, trigger) => {
    return {
      get() {
        track(); // 追踪依赖
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger(); // 触发更新
        }, delay);
      }
    };
  });
}

// 在组件中使用
<template>
  <input v-model="debouncedValue" placeholder="防抖输入">
  <p>实际值: {{ debouncedValue }}</p>
</template>

<script>
import { ref } from 'vue'
import { useDebounce } from './composables/useDebounce'

export default {
  setup() {
    const value = ref('');
    const debouncedValue = useDebounce(value, 500);
    
    return {
      debouncedValue
    };
  }
}
</script>

Vue.js 的响应式系统是其强大功能的核心,通过深入理解其工作原理和最佳实践,开发者可以构建出高性能、可维护的应用程序。