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

相关推荐
IT_陈寒14 分钟前
Python多进程池的坑:子进程竟然不会退出
前端·人工智能·后端
snow@li14 分钟前
前端:全景深度分析/前端岗位起源,以及“美国没有前端岗位”的真相
前端
恋猫de小郭15 分钟前
Gradle 9.7.0 将提速 Android 构建,Sync 提升接近一倍
android·前端·flutter
lilian23317 分钟前
Harmony os 技术实战|拼豆制图48:把个人页字符串路由改成可穷尽的类型协议
前端·华为·harmonyos
咏方舟【长江支流】27 分钟前
【前端2】单据编辑 -订单单据主子表,万能模板行,自定义添加行
前端·前端框架·状态模式·咏方舟-长江支流
qq_4523962330 分钟前
第六篇:《CI/CD 流水线:从前端构建到自动化部署》
前端·ci/cd·自动化
咏方舟【长江支流】34 分钟前
【前端1】单据编辑 -EasyUI/Vue/React/Bootstrap 主流框架实现订单单据主子表显示和编辑比较,哪个你最易入门?
前端·vue.js·easyui·咏方舟-长江支流·userbaodatagrid
艾莉丝努力练剑34 分钟前
【AI大模型接入SDK】C++日志体系与spdlog封装的理论部分
开发语言·c++·ide·人工智能·学习·面试·trae
m0_5791466536 分钟前
Element UI 表格合并单元格完全指南:从原理到实战
前端·vue.js·elementui
sibylyue40 分钟前
# Web端流媒体JS播放器开源库
前端·javascript·开源