Vue computed 与 methods 的本质差异

在日常开发里,我们经常会用 computed 来声明派生数据,用 methods 来放置纯函数。本文从 依赖追踪、Watcher 实例、求值策略、代理模式 四个维度,讲解两者的差异。

1. 语法糖背后的不同契约

js 复制代码
// computed
fullName() {
  return this.first + this.last
}

// methods
fullName() {
  return this.first + this.last
}

虽然书写形式只差一个关键字,但 Vue 在初始化阶段对两者的处理路径完全不同:

  • methods :把函数做一次 bind(this),随后把引用挂到实例上,任务结束。
  • computed :为每个属性创建专用 Watcher ,并引入 惰性求值 + 脏检查 机制,再代理到实例。

2. 计算属性的运行时三件套

当组件进入 initState 阶段,执行顺序是 initProps → initMethods → initData → initComputed → ...。在 initComputed 内部发生三件事:

2.1 创建惰性 Watcher

js 复制代码
// 简化源码
const watcher = new Watcher(
  vm,
  getter,          // 用户声明的函数
  noop,
  { lazy: true }   // 关键 flag
)

lazy: true 把 Watcher 的求值策略从 立即执行 改为 按需触发。因此:

  • watcher.value 初始为 undefined
  • watcher.dirty 初始为 true

2.2 代理读操作

随后 Vue 使用 Object.defineProperty 把计算属性挂到实例上:

js 复制代码
Object.defineProperty(vm, key, {
  enumerable: true,
  configurable: true,
  get() {
    if (watcher.dirty) {
      watcher.evaluate()   // 真正运行 getter
      watcher.dirty = false
    }
    if (Dep.target) {
      watcher.depend()     // 向上传递依赖
    }
    return watcher.value
  },
  set: setter || noop
})

访问 this.fullName 时,只要 dirty === false,就直接返回缓存值;反之则重新求值。

2.3 依赖链的"双重收集"

运行 getter 时,内部对 firstlast 的读取会触发这些字段的 Dep.depend()

由于 Dep.target 此时指向计算属性 Watcher,字段会把该 Watcher 记录进自己的订阅列表。

但视图同样需要知道字段变化,因此计算属性 Watcher 还会把渲染 Watcher 登记为下游

js 复制代码
// 在 getter 收集阶段
if (Dep.target) {
  watcher.depend()   // 让渲染 Watcher 也订阅计算属性依赖
}

最终形成一条链:

复制代码
响应式字段  →  计算属性 Watcher  →  渲染 Watcher

3. 更新策略

firstlast 变化时,触发顺序如下:

  1. 字段 Dep 通知所有订阅者
  2. 计算属性 Watcher 的 update() 被调用
  3. 由于 lazy: trueupdate 仅把 dirty 置为 true不会立即运行 getter
  4. 渲染 Watcher 的 update() 随后触发,组件进入异步队列
  5. 下一轮 flush 时,组件重新渲染,再次读取计算属性
  6. 此时 dirty === true,触发 evaluate()重新求值并缓存

这种"标记-再求值"策略把计算量压缩到真正需要视图刷新的时刻,避免无谓的中间计算。

4. methods 的极简路径对比

js 复制代码
// 伪代码
for (const key in methods) {
  vm[key] = methods[key].bind(vm)
}

没有 Watcher、没有 Dep、没有缓存,也没有任何惰性策略;每次模板调用都实打实地执行一次函数。

因此:

  • 函数内部若访问了响应式数据,不会被任何系统收集;
  • 每次渲染都会重新运行,无法享受"依赖不变即跳过"的优化。

5. setter 的语义补充

计算属性支持 { get, set } 写法:

js 复制代码
fullName: {
  get() { return this.first + this.last },
  set(v) {
    [this.first, this.last] = v.split(' ')
  }
}

当执行 this.fullName = 'A B' 时,代理的 set 逻辑被直接调用,不经过 Watcher 系统,仅负责把值写回源字段,随后由源字段的响应式系统触发视图更新。

6. 实战启示

  1. 纯展示型派生值computed,避免在模板中调用 methods 造成重复计算。
  2. 事件回调 / 工具函数methods,它们天然不需要缓存。
  3. 若计算属性 getter 开销极大,可结合 watch 手动控制更新时机,或拆分更细粒度的计算属性。
  4. 永远不要在 computed 里做副作用(异步、DOM 操作),那会破坏缓存语义并引发难以追踪的 bug。

总结

computedmethods 的差异不止"有没有缓存",而是 "是否参与依赖追踪与惰性求值"

前者通过 专用 Watcher + dirty 标记 + 双重依赖收集,把"值派生"纳入响应式闭环;后者只是一段普通的绑定函数,与响应式系统零耦合。

相关推荐
vivo互联网技术2 分钟前
EMNLP 2025|vivo 等提出 DiMo-GUI:模态分治+动态聚焦,GUI 智能体推理时扩展的新范式
前端·人工智能·agent
本末倒置18318 分钟前
Svelte邪修的JSDoc,到底是个啥?
前端·javascript·面试
李明卫杭州24 分钟前
CSS中的background-clip详解
前端·javascript
林太白30 分钟前
Vite+React+ts项目搭建(十分钟搭建个最新版React19项目吧)
前端·后端·react.js
Mr. Cao code31 分钟前
Nginx与Apache:Web服务器性能大比拼
运维·服务器·前端·nginx·apache
中微子42 分钟前
用 ECharts + React 打造环形饼图:从 0 到 1 入门
前端
彭于晏爱编程1 小时前
密码的,YOU不能不知道的Next.jsSSR(服务端渲染)
前端·javascript·react.js
再学一点就睡1 小时前
手撕前端常用 7 种设计模式:从原理到实战,附完整代码案例
前端·设计模式
龙在天1 小时前
vue3如何封装统一的弹窗
前端
一枚前端小能手1 小时前
🚀 应用出了问题你都不知道,别着急我来帮你
前端·监控