在日常开发里,我们经常会用 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
变化时,触发顺序如下:
- 字段 Dep 通知所有订阅者
- 计算属性 Watcher 的
update()
被调用 - 由于
lazy: true
,update
仅把dirty
置为true
,不会立即运行 getter - 渲染 Watcher 的
update()
随后触发,组件进入异步队列 - 下一轮 flush 时,组件重新渲染,再次读取计算属性
- 此时
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. 实战启示
- 纯展示型派生值 用
computed
,避免在模板中调用methods
造成重复计算。 - 事件回调 / 工具函数 用
methods
,它们天然不需要缓存。 - 若计算属性 getter 开销极大,可结合
watch
手动控制更新时机,或拆分更细粒度的计算属性。 - 永远不要在
computed
里做副作用(异步、DOM 操作),那会破坏缓存语义并引发难以追踪的 bug。
总结
computed
与 methods
的差异不止"有没有缓存",而是 "是否参与依赖追踪与惰性求值" 。
前者通过 专用 Watcher + dirty 标记 + 双重依赖收集,把"值派生"纳入响应式闭环;后者只是一段普通的绑定函数,与响应式系统零耦合。