Vue2 $set 深度解析 + 批量更新全套优化方案(原理+实战+踩坑+面试)

Vue2 $set 深度解析 + 批量更新全套优化方案(原理+实战+踩坑+面试)

文章简介

在 Vue2 项目开发中,$set 是解决对象新增属性、数组索引修改不触发响应式的核心 API。实际业务里,批量循环调用 $set 经常出现页面卡顿;同时业内流传「批量更新优先整体替换数组」,但在 ElementUI Table、Vxe-Table 等大数据表格场景下,整体替换反而卡顿更严重。

本文从底层响应式原理入手,剖析卡顿根源,依次讲解多套优化方案,并解答「直接赋值和 Object.assign 区别」等高频疑问,所有代码均经过生产环境验证,同时汇总踩坑点与面试真题,适合日常开发与面试复习。

运行环境:Vue2(基于 Object.defineProperty 实现响应式)

目录

  1. 一、Vue2 $set 底层原理与卡顿根源
  2. 二、为什么表格场景整体替换数组反而更卡?
  3. 三、基础优化方案(3套极简实战方案)
  4. 四、解惑:为什么推荐 Object.assign?直接赋值不行吗?
  5. 五、终极通用方案:assign + $set 智能混用(自动区分新旧属性)
  6. 六、拓展:搭配 $nextTick 优化海量数据更新
  7. 七、全方案选型对照表
  8. 八、高频踩坑总结
  9. 九、面试常考题汇总
  10. 十、全文总结

一、Vue2 $set 底层原理与卡顿根源

1.1 Vue2 响应式短板

Vue2 采用 Object.defineProperty 劫持对象属性实现响应式,存在两个原生缺陷:

  1. 直接给响应式对象新增属性,无法完成劫持,视图不会更新;
  2. 通过索引直接修改数组元素,同样无法劫持,视图不会更新。

$set 就是为了解决以上两个问题而生。

1.2 $set 核心执行逻辑

每次执行 this.$set(obj, key, value),内部主要做两件事:

  1. 调用 defineReactive 为属性绑定 getter/setter,将其转为响应式属性;
  2. 执行 dep.notify(),通知所有依赖(页面视图、watchcomputed)执行更新。

1.3 真正的卡顿原因(核心重点)

很多开发者误以为卡顿是视图多次渲染,实际并非如此

  1. 视图渲染 :Vue 会将同一事件循环内的渲染 watcher 存入异步队列并自动去重。哪怕循环上千次 $set视图最终也只会渲染 1 次
  2. 性能瓶颈 :每一次 $set 都会触发 dep.notify(),而 watchcomputed 不会被去重合并。调用多少次 $set,回调函数就会执行多少次,大量重复逻辑阻塞主线程,这才是卡顿的根本原因。

二、为什么表格场景整体替换数组反而更卡?

不少场景下使用 list = list.map(...) 整体替换数组写法简洁,但大数据表格、复杂自定义行组件严禁使用,原因如下:

  1. 数组引用变更,触发全量重渲染
    整体替换会改变数组内存引用,Vue 会判定整份列表数据全部变更,进而销毁原有全部 DOM,重新创建
  2. 表格组件渲染成本极高
    ElementUI、VxeTable 等表格除了渲染单元格,还需要实时计算行高、列宽、固定列、合并单元格、滚动条位置等,全量重建 DOM 会产生巨额性能开销。
  3. :key="index" 加剧卡顿
    若列表使用数组索引作为 key,整体替换后 key 全部失效,DOM 完全无法复用,卡顿问题会进一步放大。

场景区分

  • ✅ 推荐整体替换:简单文本列表、下拉选择、少量数据展示;
  • ❌ 禁止整体替换:大数据表格、带复杂自定义行的列表。

三、基础优化方案(3套极简实战方案)

针对「循环 $set 频繁触发 watch/computed 卡顿」问题,整理 3 套低侵入、易上手的基础方案,可根据业务场景直接选用。

方案一:状态锁拦截(新增属性/必须使用 $set 场景 · 推荐)

适用场景

需要新增响应式属性、必须依赖 $set;大数据表格、复杂列表通用;不想修改底层响应式源码。

实现思路

定义一个更新状态标识,批量更新前「上锁」,更新完成后「解锁」;在 watch 中增加状态判断,更新过程中直接跳过回调逻辑,避免重复执行。

完整代码
javascript 复制代码
export default {
  data() {
    return {
      isUpdating: false, // 批量更新状态锁
      tableList: []
    }
  },
  methods: {
    batchUpdate() {
      // 1. 上锁:标记当前正在批量更新
      this.isUpdating = true

      // 循环执行 $set 新增/修改属性
      this.tableList.forEach(item => {
        this.$set(item, 'status', 1)
        this.$set(item, 'tag', '已完成')
      })

      // 2. 下一帧解锁,恢复监听逻辑
      this.$nextTick(() => {
        this.isUpdating = false
      })
    }
  },
  watch: {
    tableList: {
      handler(newVal) {
        // 批量更新中,直接跳过,防止重复执行
        if (this.isUpdating) return
        // 原有正常业务逻辑
        console.log('列表数据变更', newVal)
      },
      deep: true
    }
  }
}
优缺点

✅ 代码简单、无黑魔法、DOM 局部复用,表格场景性能优秀;

❌ 需要在关联的 watch 中增加一行状态判断。


方案二:非响应式副本更新(零改造 · 简单列表专用)

适用场景

简单列表、少量数据;不想修改任何 watch / computed 代码,追求极简开发。

禁止场景

大数据表格、复杂行组件(会触发全量 DOM 重建)。

实现思路

通过深拷贝生成非响应式数据副本,在副本中批量修改数据(全程不触发任何响应式回调),修改完成后一次性赋值给原数组,仅触发 1 次全局更新。

完整代码
javascript 复制代码
methods: {
  batchUpdate() {
    // 1. 深拷贝生成非响应式副本,切断响应式依赖
    const tempList = JSON.parse(JSON.stringify(this.tableList))

    // 2. 自由修改数据,无需 $set,不触发 watch
    tempList.forEach(item => {
      item.status = 1
      item.tag = '已完成'
    })

    // 3. 一次性赋值,仅触发 1 次响应式更新
    this.tableList = tempList
  }
}
优缺点

✅ 零代码侵入、无需改造监听函数,中间过程无重复回调;

❌ 改变数组引用,表格会全量重渲染;超大数组深拷贝存在轻微性能损耗。


方案三:Object.assign 批量赋值(仅修改已有属性 · 性能最优)

适用场景

只修改对象已存在的属性,无需新增字段,表格高频更新首选方案。

实现思路

对于已有属性,放弃循环 $set,使用 Object.assign 批量合并属性,减少响应式通知次数,DOM 完全局部复用。

完整代码
javascript 复制代码
methods: {
  batchUpdate() {
    this.tableList.forEach(item => {
      // 批量修改已有属性,性能最优
      Object.assign(item, {
        status: 1,
        tag: '已完成',
        remark: '批量更新'
      })
    })
  }
}
重要避坑

Object.assign 无法为对象新增响应式属性

  1. 修改已有属性:正常触发响应式,视图更新 ✅;
  2. 新增不存在的属性:本次渲染临时生效,但新增字段无响应式,后续单独修改视图不会更新 ❌。
优缺点

✅ 性能最佳、代码简洁、DOM 局部复用;

❌ 不支持新增响应式属性。


四、解惑:为什么推荐 Object.assign?直接赋值不行吗?

很多同学会产生疑问:既然直接赋值也能修改属性并触发视图更新,为什么还要刻意使用 Object.assign?这里澄清核心误区。

4.1 核心结论

修改对象已有响应式属性时,直接赋值 和 Object.assign 效果完全一致,两者都会正常触发响应式、更新视图,性能也没有区别。

javascript 复制代码
// 写法1:直接赋值(完全可行)
item.name = "张三";
item.age = 18;
item.status = 1;

// 写法2:Object.assign(同样可行)
Object.assign(item, {
  name: "张三",
  age: 18,
  status: 1
});

4.2 推荐使用 Object.assign 的真正原因

并非响应式能力有差异,而是出于工程化与代码规范考量:

  1. 批量赋值更优雅:一次性更新多个字段时,聚合式写法结构更清晰,可读性更强;
  2. 便于统一处理:可将更新字段封装为独立对象,方便做字段过滤、数据校验、日志埋点等通用逻辑;
  3. 适配批量更新方案:能够完美配合「自动区分新旧属性」的批量更新逻辑,实现代码统一管理。

4.3 边界区分(必记)

  1. 修改已有属性直接赋值 / Object.assign 二选一即可;
  2. 给对象新增属性 :两者都无法生成响应式属性,必须使用 $set

一句话总结

已有属性:直接赋值 = Object.assign;

新增属性:只有 $set 能保证响应式。


五、终极通用方案:assign + $set 智能混用(自动区分新旧属性)

5.1 方案背景

前面三种基础方案都存在局限性:人工区分新旧属性代码冗余、Object.assign 不支持新增属性、纯循环 $set 易触发卡顿。

本方案实现自动判断属性是否存在

  • 已有属性 → 使用 Object.assign 保证运行性能;
  • 新增属性 → 使用 $set 保证响应式完整性;
    一套代码适配全场景,兼顾性能、功能与可维护性。

5.2 工具函数实现

提供组件局部使用全局挂载两种方式,按需选择。

方式1:组件内局部方法(单组件使用)
javascript 复制代码
export default {
  methods: {
    /**
     * 智能更新响应式对象属性
     * @param {Object} target 目标响应式对象
     * @param {Object} updateData 待更新键值对
     */
    updateReactiveProps(target, updateData) {
      // 边界校验
      if (!target || typeof target !== 'object' || !updateData) return

      const existProps = {} // 存储已有属性
      const newProps = {}   // 存储新增属性

      // 自动分类新旧属性
      Object.keys(updateData).forEach(key => {
        if (target.hasOwnProperty(key)) {
          existProps[key] = updateData[key]
        } else {
          newProps[key] = updateData[key]
        }
      })

      // 1. 批量更新已有属性(高性能)
      Object.assign(target, existProps)
      // 2. 逐个处理新增属性(保证响应式)
      Object.keys(newProps).forEach(key => {
        this.$set(target, key, newProps[key])
      })
    }
  }
}
方式2:全局挂载(整个项目通用,在 main.js 配置)
javascript 复制代码
import Vue from 'vue'

// 全局注册智能更新方法
Vue.prototype.$updateReactiveProps = function (target, updateData) {
  if (!target || typeof target !== 'object' || !updateData) return

  const existProps = {}
  const newProps = {}

  Object.keys(updateData).forEach(key => {
    if (target.hasOwnProperty(key)) {
      existProps[key] = updateData[key]
    } else {
      newProps[key] = updateData[key]
    }
  })

  Object.assign(target, existProps)
  Object.keys(newProps).forEach(key => {
    this.$set(target, key, newProps[key])
  })
}

5.3 实战调用示例

javascript 复制代码
export default {
  data() {
    return {
      tableList: [
        { id: 1, name: '小明', age: 20 },
        { id: 2, name: '小红', age: 22 }
      ]
    }
  },
  methods: {
    batchUpdate() {
      this.tableList.forEach(item => {
        // 无需手动区分新旧属性,工具函数自动处理
        this.updateReactiveProps(item, {
          name: '用户' + item.id, // 已有属性 → Object.assign
          age: item.age + 1,      // 已有属性 → Object.assign
          status: '正常'          // 新增属性 → $set
        })
      })
    }
  }
}

5.4 方案优势

  1. 自动分类新旧属性,减少人工编码成本;
  2. 已有属性走 Object.assign,避免 watch/computed 频繁执行;
  3. 新增属性走 $set,保证完整响应式;
  4. DOM 局部更新,大数据表格流畅不卡顿;
  5. 无侵入改造,支持全局复用。

六、拓展:搭配 $nextTick 优化海量数据更新

如果一次性更新上万条海量数据 ,可结合 $nextTick 将所有更新逻辑放入微任务中执行:

  1. 避免 JS 主线程与浏览器重排交替执行;
  2. 所有逻辑执行完毕后,浏览器仅执行一次页面重排,进一步提升流畅度。

示例代码

javascript 复制代码
methods: {
  batchUpdate() {
    this.$nextTick(() => {
      this.tableList.forEach(item => {
        this.$updateReactiveProps({
          name: '测试用户',
          status: '在线'
        })
      })
    })
  }
}

七、全方案选型对照表

方案 核心用法 响应式完整性 性能表现 最佳适用场景 不推荐场景
状态锁拦截 isUpdating + 循环 $set 完整 优秀 需新增属性、大数据表格、复杂列表
非响应式副本 深拷贝 + 一次性赋值 完整 良好 简单列表、少量数据、不想改造 watch 大数据表格
Object.assign 批量合并属性 新增属性无响应式 最优 仅修改已有字段、表格高频更新 需要新增响应式属性
智能区分方案 自动分类 + assign + $set 完整 优秀 新旧属性混合更新、全场景通用

八、高频踩坑总结

  1. hasOwnProperty 特性

    工具函数使用 hasOwnProperty 仅判断对象自身属性,不会读取原型链属性,符合 Vue 响应式对象使用规范,无需额外处理。

  2. 数组索引场景

    本文所有方案主要面向普通对象 ;Vue2 中修改数组索引,依旧需要单独使用 $set

    javascript 复制代码
    this.$set(arr, 0, '新值')
  3. 深层嵌套对象

    工具函数仅处理对象第一层属性 ;如果是多层嵌套对象,内层字段新增时,仍需要单独调用 $set

  4. Object.assign 新增属性坑

    牢记:Object.assign 只能修改已有属性,新增的字段不具备响应式。

  5. 表格慎用整体替换

    大数据表格优先局部更新,坚决避免数组整体替换导致的全量 DOM 重建。


九、面试常考题汇总

1. 循环多次调用 $set,视图会渲染多少次?卡顿根源是什么?

答:同一事件循环内视图仅渲染 1 次,Vue 会对渲染 watcher 去重;卡顿根源是每次 $set 都会触发 dep.notify(),导致 watchcomputed 回调重复执行,阻塞主线程。

2. 直接赋值、Object.assign、$set 三者区别?

答:修改已有属性时,直接赋值和 Object.assign 效果一致,均可触发响应式;新增属性时,前两者无法生成响应式,必须使用 $set。批量场景推荐 Object.assign,代码更规范。

3. 为什么大数据表格不建议整体替换数组?

答:整体替换会改变数组引用,Vue 触发全量 DOM 销毁重建;表格需要重新计算列宽、固定列、滚动条等,渲染开销极大,局部更新可复用 DOM,性能更优。

4. 自动区分属性方案的设计思路?

答:通过 hasOwnProperty 判断字段是否为对象原有属性,已有属性使用 Object.assign 提升性能,新增属性使用 $set 保证响应式,兼顾性能与功能。

5. Vue3 还需要使用 $set 吗?

答:不需要。Vue3 基于 Proxy 实现响应式,天然支持对象新增属性、数组索引修改,直接赋值即可。


十、全文总结

  1. Vue2 循环 $set 卡顿并非视图渲染导致 ,核心原因是 watch/computed 被频繁触发;
  2. 简单列表可使用「非响应式副本」,仅修改已有字段优先 Object.assign,必须新增属性推荐「状态锁」;
  3. 新旧属性混合更新的通用场景,首选自动区分新旧属性方案,是 Vue2 批量更新最优解;
  4. 大数据表格禁止无脑整体替换数组,优先局部更新复用 DOM;
  5. 海量数据批量更新,可搭配 $nextTick 优化代码执行时机,提升页面流畅度。

实战口诀:旧属性 assign 提速,新属性 $set 兜底,表格拒绝全量替换,批量更新从此不卡顿。

相关推荐
Xzh04231 小时前
Redis黑马点评 实战复盘与面试高频考点详解
java·数据库·redis·面试
SiYuanFeng2 小时前
百度网盘【搜索/查找】如何限定在当前文件夹下搜索
面试
Ws_3 小时前
C# 桌面端开发工程师面试题 + 参考答案
开发语言·面试·c#
黄啊码3 小时前
【黄啊码】拉勾倒了,但你的简历早就不该在招聘软件上了
人工智能·面试
Aphasia3113 小时前
Vite配置代理和后端服务器CORS
面试
触底反弹3 小时前
dom操作这篇文章就够了
javascript·面试
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第86题】【Mysql篇】第16题:MySQL 中锁的种类与行锁实现原理?
java·开发语言·数据库·mysql·面试
我爱cope3 小时前
【Agent智能体14 | 工具使用-如何创建工具】
人工智能·语言模型·职场和发展
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第85题】【Mysql篇】第15题:MySQL 的事务中,幻读是怎么解决的?
java·开发语言·数据库·mysql·面试