深入浅出 Vue 3 核心黑魔法:彻底搞懂什么是「惰性响应」?

2 阅读5分钟

在深入研究 Vue 3 性能优化时,你一定经常听到一个高频词——惰性响应(Lazy Reactivity / Lazy Evaluation)

Vue 3 之所以在面对海量数据和复杂业务时依然能保持极高的运行效率,很大程度上得益于其底层全面贯彻的**“按需执行”“不见兔子不撒鹰”**的惰性设计思想。

今天这篇文章,我们就由浅入深,彻底拆解 Vue 3 中「惰性响应」的三个核心应用场景。


一、 计算属性(Computed)的惰性求值

Vue 3 的 computed 是惰性响应最标准的体现。它的核心特点是:如果你在模板或代码里没有用到这个计算属性,它就永远不会执行计算。

1. 核心代码示例

import { ref, computed } from 'vue'

const count = ref(1)

// 创建一个计算属性
const doubleCount = computed(() => {
  console.log('--- 计算属性执行了!---') 
  return count.value * 2
})

2. 它是如何「惰性」的?

  • 初始化时:当代码运行到 computed(...) 这一行时,控制台什么都不会打印。因为此时没有任何地方在读取 doubleCount,Vue 认为没必要浪费 CPU 算力去执行里面的函数。
  • 首次读取时:当你在代码里写了 console.log(doubleCount.value),或者在 template 模板里渲染了 {{ doubleCount }},此时函数第一次执行,控制台才会打印出日志。
  • 依赖改变时:如果你让 count.value = 2,此时 doubleCount 内部的函数依然不会立刻执行!Vue 只是在底层把它的状态标记为“脏数据(dirty = true)”。只有当你下一次再次读取 doubleCount.value 时,它才会临门一脚触发重新计算。

💡 小贴士(设计思考): 假设你的计算属性需要执行非常复杂的循环或大数据量过滤。如果依赖项频繁变动,但页面上根本没展示这个数据,那么每一次变动都去计算就会导致页面卡顿。惰性响应确保了 “用不到就不算,用到才算” 的极致省电模式。

二、 侦听器(Watch)的惰性侦听

在 Vue 3 中,默认的 watch 也是惰性的。这意味着:当你声明一个侦听器时,它不会立刻执行,而是等到被侦听的数据真正发生变化时,才会触发回调。

import { ref, watch } from 'vue'

const keyword = ref('')

watch(keyword, (newVal) => {
  console.log(`关键字变了,新值是: ${newVal}`)
})
// 刚进入页面时,这行 console.log 绝对不会执行

🛠️ 延伸扩展:如何打破惰性?

如果你希望在组件初始化时就立刻执行一次(非惰性),你可以:

  1. watch 显式地传入 { immediate: true } 属性。
  2. 直接使用天然非惰性的 watchEffect

三、 底层 Proxy 的「惰性劫持」(超核心!)

除了应用层的 computedwatch,Vue 3 的底层响应式系统(基于 Proxy)本身也贯彻了惰性的思想,这与 Vue 2 的 Object.defineProperty 有着本质的区别。

一句话总结 Vue 3 的底层惰性: “没访问到深层对象,就不对深层对象做响应式包装。”

为了让你秒懂这句话,我们来做个直观的对比:

1. 回顾 Vue 2(非惰性):全家桶式“一拥而上”

在 Vue 2 中,组件初始化时,Vue 会立刻启动一个深度遍历函数(递归)。

假设接口返回了一个庞大的嵌套对象:

const data = {
  id: 1,
  user: {
    name: '张三',
    history: {
      loginLogs: [/* 假设这里有一万条登录日志数据... */]
    }
  }
}

Vue 2 的做法:不管你页面上有没有用到 loginLogs,Vue 2 会在开局硬生生地把这一万条日志数据全部递归遍历一遍,给里面的每一个属性都加上 gettersetter。如果数据量极大,页面在刚加载的时候就会明显卡顿。

2. 拥抱 Vue 3(底层惰性):临门一脚才包装

Vue 3 改用了 ProxyProxy 的特点是:它不关心对象里面藏了多少层,它只守在最外层的大门口。

同样是上面的大对象,Vue 3 通过 reactive() 包装:

  • 初始化时(极其轻量) :Vue 3 只把最外层(也就是 iduser 这一层)包装成 Proxy。至于内部更深的 historyloginLogs,Vue 3 压根连看都不看一眼,它们目前还是标准的、毫无响应式功能的普通 JS 对象。
  • 按需包装(依赖触发) :只有当你的代码真正运行并访问到深层数据时,例如:
console.log(state.user.name)
  • 由于你访问了 state.user,这就触发了最外层 Proxy 的 get 拦截器。

    此时 Vue 3 的底层源码会执行一个非常聪明的逻辑: 当它发现你拿到了 user,并且 user 是个对象,它会当场、临时调用 reactive(user) 把这一层包装成全新的 Proxy 返回给你。

💾 用一个通俗的比喻来理解

  • Vue 2 就像是「传统实体书店」: 书店进了一万本书(深层数据)。在开店前,老板必须雇佣员工把这一万本书全部拆封、盖上书店的印章、贴上防盗芯片。不管客人们今天来不买,这个工作量开门前必须全做完,导致开门前的准备时间极长。
  • Vue 3 就像是「仓储超市的收银台」: 仓库里有一万件商品整齐地码在推车里。收银员(Proxy)只坐在大门口的收银台前。推车深处的商品收银员根本不理。只有当你把商品一件件拿出来放到扫描枪下面(触发了 get 访问)时,收银员才会“哔”的一声把这件商品录入系统(临时包装成响应式)。

四、 总结:惰性响应带来的红利

Vue 3 这种全面惰性化的设计,带来了两个立竿见影的好处:

  1. 极速的初始化性能:无论后端接口返回的数据多么深、多么庞大,Vue 3 都能瞬间完成响应式初始化,省去了大量的递归性能消耗。
  2. 零内存浪费:如果有些深层数据只是存在变量里供代码逻辑偶尔调用,而没有在界面上高频渲染,那么它们就永远保持普通对象的身份,不占用响应式系统的额外内存。

不见兔子不撒鹰,这就是 Vue 3 惰性响应的优雅之处。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注三连!我们在下一篇 Vue 3 底层源码拆解中再见!