Vue.observable实现vue原生轻量级状态管理详解

一、Vue.observable 基本定义

其实在日常工作中,Vue.observable 用的并不多,但是是一个非常轻量级的vue原生状态管理工具。它是 Vue 2.x 提供的全局 API ,用于将一个普通对象转化为响应式对象,返回的对象可以直接被 Vue 追踪依赖、触发视图更新。它是 Vue 响应式系统的底层能力暴露,也是 Vuex/组件 data 响应式的核心底层逻辑。

核心特征
  • 作用域 :全局 API(Vue 2.x),Vue 3 中被 reactive 替代(底层由 Object.defineProperty 改为 Proxy);
  • 响应式粒度 :针对对象的属性级别做响应式劫持;
  • 返回值:原对象的响应式代理(并非新对象,修改原对象也会触发更新)。
语法
js 复制代码
Vue.observable(obj)
  • 参数:obj --- 普通纯对象(不支持基本类型、数组需特殊处理);ps:因为它时基于Object.definedProperty实现的,所以为了能够劫持数据实现修改和获取,在实际应用时对基本类型加一层{}就可以了;
  • 返回值:响应式对象。

二、底层实现原理

Vue.observable 的核心逻辑和组件 data 响应式完全一致,基于 Object.defineProperty 劫持属性的 getter/setter

核心流程
  1. 创建 Observer 实例 :对传入的对象递归遍历,为每个属性定义 getter/setter
  2. 依赖收集(getter) :当组件渲染访问该属性时,getter 会收集当前组件的 Watcher(依赖)到 Dep(依赖管理器);
  3. 派发更新(setter) :当属性值修改时,setter 触发 Dep 通知所有收集的 Watcher 执行更新(重新渲染组件)。
简化源码(Vue 2.x 核心逻辑)
js 复制代码
// 核心:为对象属性定义响应式
function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  const childOb = observe(val);
  const dep = new Dep(); // 依赖管理器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集:将当前 Watcher 加入 Dep
      if (Dep.target) {
        dep.depend();
        if (childOb) childOb.dep.depend(); // 嵌套对象的依赖收集
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 新值如果是对象,递归做响应式
      observe(newVal);
      // 派发更新:通知所有 Watcher 执行更新
      dep.notify();
    },
  });
}

// 对外暴露的 observable 方法
Vue.observable = function (obj) {
  observe(obj); // 为对象创建 Observer 实例
  return obj;
};

// 递归观察对象
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  return new Observer(obj);
}

class Observer {
  constructor(value) {
    this.dep = new Dep();
    // 遍历对象属性,逐个做响应式
    Object.keys(value).forEach(key => {
      defineReactive(value, key, value[key]);
    });
  }
}

三、核心使用场景

Vue.observable 主要用于轻量级状态管理,替代 Vuex 适用于小型应用/组件间共享状态的场景。

场景 1:全局共享状态(替代简易 Vuex)

适用于无需模块化、复杂逻辑的全局状态(如用户信息、主题切换、全局加载状态)。

js 复制代码
// store.js - 全局状态管理
import Vue from 'vue';

// 创建响应式状态
export const state = Vue.observable({
  userInfo: null,
  theme: 'light',
  loading: false,
});

// 定义修改状态的方法(规范操作)
export const mutations = {
  setUserInfo(info) {
    state.userInfo = info;
  },
  toggleTheme() {
    state.theme = state.theme === 'light' ? 'dark' : 'light';
  },
  setLoading(flag) {
    state.loading = flag;
  },
};

组件中使用:

vue 复制代码
<template>
  <div :class="state.theme">
    <p>{{ state.userInfo?.name }}</p>
    <button @click="mutations.toggleTheme">切换主题</button>
  </div>
</template>

<script>
import { state, mutations } from './store.js';

export default {
  computed: {
    // 直接访问响应式状态(依赖收集)
    state() {
      return state;
    },
  },
  methods: {
    mutations,
  },
};
</script>
场景 2:跨组件共享状态(非父子通信)

无需通过 props/$emit/eventBus,直接通过响应式对象共享状态:

js 复制代码
// 共享状态文件:sharedState.js
import Vue from 'vue';
export const sharedState = Vue.observable({
  count: 0,
});

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

组件 A:

vue 复制代码
<template>
  <div>组件A:{{ sharedState.count }}</div>
</template>
<script>
import { sharedState } from './sharedState.js';
export default {
  computed: {
    sharedState() {
      return sharedState;
    },
  },
};
</script>

组件 B:

vue 复制代码
<template>
  <button @click="increment">点击+1</button>
</template>
<script>
import { increment } from './sharedState.js';
export default {
  methods: { increment },
};
</script>
场景 3:组件内逻辑复用(替代 mixin)

将组件内的响应式状态抽离,实现逻辑复用:

js 复制代码
// useCounter.js
import Vue from 'vue';
export default function useCounter() {
  const state = Vue.observable({ count: 0 });
  const increment = () => state.count++;
  const decrement = () => state.count--;
  return { state, increment, decrement };
}

组件中使用:

vue 复制代码
<script>
import useCounter from './useCounter.js';
export default {
  setup() { // Vue 2.7+ 支持 setup
    const { state, increment, decrement } = useCounter();
    return { state, increment, decrement };
  },
};
</script>

四、关键注意事项(避坑点)

由于底层基于 Object.definePropertyVue.observable 存在以下局限性:

1. 仅支持对象,不支持基本类型

基本类型(字符串、数字、布尔)无法直接做响应式,必须包装为对象:

js 复制代码
// ❌ 无效:基本类型无属性可劫持
const num = Vue.observable(123); 

// ✅ 有效:包装为对象
const numObj = Vue.observable({ value: 123 });
2. 新增/删除属性不触发响应式

由于vueObject.defineProperty 只能劫持已存在的属性,新增属性跟vue2更新对象/数组数据时一样需用 Vue.set,删除需用 Vue.delete

js 复制代码
const state = Vue.observable({ name: '张三' });

// ❌ 新增属性无响应式
state.age = 18; 

// ✅ 用 Vue.set 新增响应式属性
Vue.set(state, 'age', 18);

// ❌ 删除属性不触发更新
delete state.name;

// ✅ 用 Vue.delete 删除
Vue.delete(state, 'name');
3. 数组响应式限制

数组的索引修改、长度修改不触发更新,需使用数组变异方法push/pop/shift/unshift/splice/sort/reverse)或 Vue.set

js 复制代码
const list = Vue.observable({ arr: [1, 2, 3] });

// ❌ 索引修改无响应式
list.arr[0] = 100; 

// ✅ 用变异方法
list.arr.splice(0, 1, 100);

// ✅ 用 Vue.set
Vue.set(list.arr, 0, 100);
4. 解构赋值丢失响应式

解构会将响应式属性转为基本类型,脱离 getter/setter 劫持,导致修改不触发更新:

js 复制代码
const state = Vue.observable({ count: 0 });

// ❌ 解构后 count 是基本类型,修改无响应式
const { count } = state;
count++; 

// ✅ 直接访问响应式对象的属性
state.count++;
5. 嵌套对象需递归劫持

Vue.observable 会递归处理嵌套对象,但若嵌套对象是后续新增的,需手动重新劫持:

js 复制代码
const state = Vue.observable({ info: {} });

// ❌ 新增的嵌套对象无响应式
state.info.address = '北京'; 

// ✅ 先赋值响应式对象
state.info = Vue.observable({ address: '北京' });

五、Vue.observable vs Vue3 reactive/ref

Vue 3 废弃了 Vue.observable,改用 reactive/ref,核心差异如下:

特性 Vue.observable (Vue2) reactive (Vue3) ref (Vue3)
底层实现 Object.defineProperty Proxy 包装对象(value 属性)
响应式粒度 属性级别 对象级别(支持新增/删除属性) 基本类型/对象(统一.value 访问)
新增属性支持 不支持(需 Vue.set) 天然支持 支持(ref 对象的 value)
数组支持 索引修改无效(需变异方法) 天然支持索引/长度修改 支持(ref 包裹数组)
解构响应式 丢失响应式 需 toRefs 保留响应式 解构后仍需 .value 访问
基本类型支持 不支持(需包装为对象) 不支持(需 ref) 专门支持基本类型

六、总结

  1. 核心定位Vue.observable 是 Vue2 响应式系统的底层暴露,适合小型应用的轻量级状态管理,无需引入 Vuex;
  2. 核心局限 :受限于 Object.defineProperty,不支持新增属性、数组索引修改、基本类型等,需配合 Vue.set/变异方法使用;
  3. 替代方案 :Vue2.7+ 可逐步迁移到 Composition API(setup + reactive),Vue3 直接使用 reactive/ref
  4. 最佳实践
    • Vue.observable 封装全局状态时,统一通过方法修改状态(类似 Vuex mutations),避免直接修改;
    • 避免解构响应式对象,直接访问属性;
    • 新增属性必须用 Vue.set,数组修改用变异方法。

理解 Vue.observable 的原理,本质是理解 Vue 响应式系统的核心(getter/setter 劫持、依赖收集、派发更新),这也是掌握 Vue 响应式的关键。

相关推荐
严文文-Chris2 小时前
RAG关键技术要点详解
java·服务器·前端
自然 醒2 小时前
elementUI的select下拉框如何下拉加载数据?
前端·javascript·elementui
我没想到原来他们都是一堆坏人2 小时前
常用npm源与nrm
前端·npm·node.js
编代码的小王2 小时前
【无标题】
前端·css
仰望.2 小时前
vxe-table 按多个列进行分组和按多个字段进行分组的使用方式
vue.js·vxe-table
im_AMBER2 小时前
React 20 useState管理组件状态 | 解构 | 将事件处理函数作为 props 传递 | 状态提升
前端·javascript·笔记·学习·react.js·前端框架
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Tools
前端·javascript·easyui
亿元程序员2 小时前
你知道三国志战略版是怎么实现横竖屏动态切换的吗?
前端