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 时,内部对 first、last 的读取会触发这些字段的 Dep.depend() 。

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

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

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

最终形成一条链:

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

3. 更新策略

当 first 或 last 变化时,触发顺序如下:

  1. 字段 Dep 通知所有订阅者
  2. 计算属性 Watcher 的 update() 被调用
  3. 由于 lazy: true,update 仅把 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。

总结

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

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

相关推荐
Java后端的Ai之路19 分钟前
03_React_JSX
前端·react.js·前端框架
浅念-23 分钟前
动态规划专题:斐波那契数列模型 + 网格路径DP(LeetCode例题全解析)
笔记·算法·leetcode·面试·职场和发展·动态规划·推荐算法
落魄大学生之流水线上谋生计25 分钟前
接口测试实习指南:从基础方法到面试与现场难题
面试·职场和发展
颜进强37 分钟前
26 · NestJS 基础篇 · 全专栏总结:五个阶段,一次收拢
前端·后端·ai编程
风寄巴山秋38 分钟前
OpenBMC:Web 页面功能异常排查
运维·服务器·前端·架构
程序员Sunday1 小时前
Promise.all、allSettled、race、any 怎么选,失败后其他请求会怎样
开发语言·前端·javascript
niucloud-admin1 小时前
JAVA V6 多商户商城 开发文档——管理端前端
前端
weixin_422201301 小时前
如何解决小程序图标点击热区小,落点不准问题?
前端·小程序·样式·点击热区·扩大
程序员清风2 小时前
AI时代建议学点自己真正感兴趣的!
java·后端·面试
szial2 小时前
JavaScript 的 call、apply 和 bind:如何控制函数调用
开发语言·前端·javascript