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

相关推荐
程序员爱钓鱼1 小时前
Go语言实战案例 — 项目实战篇:简易博客系统(支持评论)
前端·后端·go
excel8 小时前
ES6 中函数的双重调用方式:fn() 与 fn\...``
前端
可乐爱宅着8 小时前
全栈框架next.js入手指南
前端·next.js
bobz9658 小时前
进程和线程结构体的统一和差异
面试
你的人类朋友10 小时前
什么是API签名?
前端·后端·安全
会豪12 小时前
Electron-Vite (一)快速构建桌面应用
前端
中微子12 小时前
React 执行阶段与渲染机制详解(基于 React 18+ 官方文档)
前端
唐某人丶12 小时前
教你如何用 JS 实现 Agent 系统(2)—— 开发 ReAct 版本的“深度搜索”
前端·人工智能·aigc
中微子12 小时前
深入剖析 useState产生的 setState的完整执行流程
前端
遂心_12 小时前
JavaScript 函数参数传递机制:一道经典面试题解析
前端·javascript