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 标记 + 双重依赖收集,把"值派生"纳入响应式闭环;后者只是一段普通的绑定函数,与响应式系统零耦合。

相关推荐
choke2336 小时前
[特殊字符] Python 文件与路径操作
java·前端·javascript
云飞云共享云桌面6 小时前
高性能图形工作站的资源如何共享给10个SolidWorks研发设计用
linux·运维·服务器·前端·网络·数据库·人工智能
wqq63108556 小时前
Python基于Vue的实验室管理系统 django flask pycharm
vue.js·python·django
Deng9452013146 小时前
Vue + Flask 前后端分离项目实战:从零搭建一个完整博客系统
前端·vue.js·flask
威迪斯特6 小时前
Flask:轻量级Web框架的技术本质与工程实践
前端·数据库·后端·python·flask·开发框架·核心架构
Hello.Reader6 小时前
Flink 文件系统通用配置默认文件系统与连接数限制实战
vue.js·flink·npm
wuhen_n6 小时前
JavaScript内置数据结构
开发语言·前端·javascript·数据结构
大鱼前端6 小时前
为什么我说CSS-in-JS是前端“最佳”的糟粕设计?
前端
不爱吃糖的程序媛6 小时前
Capacitor:跨平台Web原生应用开发利器,现已全面适配鸿蒙
前端·华为·harmonyos
AC赳赳老秦6 小时前
2026国产算力新周期:DeepSeek实战适配英伟达H200,引领大模型训练效率跃升
大数据·前端·人工智能·算法·tidb·memcache·deepseek