【vue篇】Vue 数组响应式揭秘:如何让 push 也能更新视图?

在 Vue 开发中,你是否思考过:

"为什么 this.items.push(newItem) 能自动更新页面?"
"Vue 是如何监听数组内部变化的?"
"sort()reverse() 为何也能触发视图更新?"

这背后是 Vue 对数组方法的精妙劫持与增强

本文将从 Object.defineProperty 的局限Vue 源码级实现,彻底解析 Vue 数组响应式的黑科技。


一、问题根源:Object.defineProperty 的缺陷

✅ Vue 2 响应式原理

Vue 2 使用 Object.defineProperty 劫持对象属性的 getset

js 复制代码
Object.defineProperty(obj, 'prop', {
  get() { /* 依赖收集 */ },
  set(newVal) { /* 派发更新 */ }
});

❌ 数组的"盲区"

js 复制代码
const arr = ['a', 'b'];
arr[2] = 'c';        // ❌ 无法触发 set
arr.length = 0;      // ❌ 无法触发 set
arr.push('d');       // ❌ 原生方法不走 set

💥 Object.defineProperty 无法监听

  • 数组索引赋值;
  • 数组长度变化;
  • 数组方法调用。

二、Vue 的解决方案:方法劫持

✅ 核心思路

"既然不能监听属性,那就劫持会改变数组的方法!"

Vue 创建了一个增强版数组原型,覆盖了 7 个会改变原数组的方法。


三、源码级实现:arrayMethods 详解

📌 步骤 1:创建增强原型

js 复制代码
// 缓存原生 Array.prototype
const arrayProto = Array.prototype;

// 创建新对象,__proto__ 指向原生原型
export const arrayMethods = Object.create(arrayProto);

💡 这样既能保留原生功能,又能扩展新逻辑。


📌 步骤 2:拦截变异方法

js 复制代码
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

methodsToPatch.forEach(function(method) {
  const original = arrayProto[method]; // 保存原生方法
  
  def(arrayMethods, method, function mutator(...args) {
    // 1. 执行原生逻辑
    const result = original.apply(this, args);
    
    // 2. 获取 Observer 实例
    const ob = this.__ob__;
    
    // 3. 处理新增元素(需要响应式化)
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args; // 新增的元素
        break;
      case 'splice':
        inserted = args.slice(2); // splice(索引, 删除数, 新增元素...)
        break;
    }
    
    // 4. 对新增元素进行响应式处理
    if (inserted) {
      ob.observeArray(inserted);
    }
    
    // 5. 通知依赖更新!
    ob.dep.notify();
    
    // 6. 返回原生方法结果
    return result;
  });
});

四、如何让数组使用增强方法?

✅ 在 Observer 中"偷天换日"

js 复制代码
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    
    // 关键:修改数组的原型
    if (Array.isArray(value)) {
      // value.__proto__ = arrayMethods
      value.__proto__ = arrayMethods;
      
      // 或对非浏览器环境使用方法拷贝
      // def(value, method, arrayMethods[method])
    } else {
      this.walk(value);
    }
  }
}

💥 这样一来,所有响应式数组调用 push 等方法时,实际执行的是增强版方法


五、7 个被劫持的方法详解

方法 是否新增元素 是否触发更新
push(...items)
pop() ✅(长度变)
shift() ✅(长度变)
unshift(...items)
splice(start, deleteCount, ...items)
sort() ✅(顺序变)
reverse() ✅(顺序变)

⚠️ 注意:popshift 虽不新增,但改变了数组,所以也要 notify


六、实战演示

📌 场景:动态添加列表项

vue 复制代码
<template>
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
  <button @click="addItem">添加</button>
</template>

<script>
export default {
  data() {
    return {
      items: [{ id: 1, name: 'A' }]
    };
  },
  methods: {
    addItem() {
      // 调用被劫持的 push
      this.items.push({ id: 2, name: 'B' });
      // 1. 执行原生 push
      // 2. observeArray([{ id: 2, name: 'B' }]) → 响应式化
      // 3. dep.notify() → 视图更新
    }
  }
}
</script>

七、Vue 3 的革命:Proxy 彻底解决

✅ Vue 3 响应式原理

js 复制代码
const arr = reactive(['a', 'b']);
arr[2] = 'c';     // ✅ Proxy 捕获 set
arr.length = 0;   // ✅ 捕获 length 变化
arr.push('d');    // ✅ 原生方法调用,但 Proxy 仍能感知数组变化

💥 Vue 3 不再需要劫持数组方法Proxy 天然支持所有变化监听。


八、其他数组操作的注意事项

❌ Vue 无法检测的操作

js 复制代码
// 1. 索引直接赋值
this.items[0] = { ... }; // ❌

// 2. 修改数组长度
this.items.length = 0;   // ❌

✅ 解决方案(Vue 2)

js 复制代码
// 使用 $set
this.$set(this.items, 0, { ... });

// 使用 splice
this.items.splice(0, 1, { ... });

// 清空数组
this.items.splice(0);
// 或
this.items = [];

💡 结语

"Vue 的数组响应式,是工程智慧的典范。"

机制 Vue 2 Vue 3
核心技术 方法劫持 + __proto__ Proxy
劫持方法 7 个变异方法 无需劫持
监听能力 部分支持 完全支持
方法 增强逻辑
push 响应式化新元素 + 通知更新
splice 响应式化新增项 + 通知更新
sort 直接触发更新(顺序变)

掌握这一机制,你就能:

✅ 理解 push 为何能更新视图;

✅ 避开 this.items[0] = value 的陷阱;

✅ 在 Vue 2 和 Vue 3 间平滑迁移。

相关推荐
LuckySusu3 小时前
【vue篇】Vue 性能优化神器:keep-alive 深度解析与实战指南
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 核心机制揭秘:为什么组件的 data 必须是函数?
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 响应式陷阱:动态添加对象属性为何不更新?如何破解?
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 异步更新之魂:$nextTick 原理与实战全解
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 条件渲染终极对决:v-if vs v-show 深度解析
前端·vue.js
LuckySusu3 小时前
【vue篇】单页 vs 多页:Vue 应用架构的终极对决
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 核心指令原理解析:v-if、v-show、v-html 的底层奥秘
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue 进阶指南:如何在自定义组件中完美使用 v-model
前端·vue.js
LuckySusu3 小时前
【vue篇】Vue v-model 深度解析:从表单到组件的双向绑定之谜
前端·vue.js