2.2 计算属性与侦听器


文档摘要

2.2 计算属性与侦听器 本节摘要:模板里放不下复杂逻辑,Vue 给出两个出口——计算属性解决"派生值"(带缓存的依赖驱动计算),侦听器解决"副作用"(数据变化时执行动作)。本节拆解计算属性缓存的实现机制、两者的适用边界,以及 nextTick 与异步更新队列的时序关系。 学习目标 阅读完本节,你应当能够: 解释计算属性的缓存何时命中、何时失效,并用代码验证; 在"方法、计算属性、侦听器"三者之间按需求正确选型; 写出带 getter/setter 的双向计算属性,以及侦听器中 immediate 与 deep 的取舍; 描述异步更新队列的时序,说明 nextTick 为什么能拿到更新后的 DOM。

2.2 计算属性与侦听器

本节摘要:模板里放不下复杂逻辑,Vue 给出两个出口——计算属性解决"派生值"(带缓存的依赖驱动计算),侦听器解决"副作用"(数据变化时执行动作)。本节拆解计算属性缓存的实现机制、两者的适用边界,以及 nextTick 与异步更新队列的时序关系。

学习目标

阅读完本节,你应当能够:

  1. 解释计算属性的缓存何时命中、何时失效,并用代码验证;
  2. 在"方法、计算属性、侦听器"三者之间按需求正确选型;
  3. 写出带 getter/setter 的双向计算属性,以及侦听器中 immediate 与 deep 的取舍;
  4. 描述异步更新队列的时序,说明 nextTick 为什么能拿到更新后的 DOM。

一、派生值的问题:模板不该有逻辑

假设要显示购物车总价:

items: [ { name: '书', price: 59, count: 2 }, { name: '笔', price: 12, count: 5 } ]

把求和表达式直接塞进模板 {{ items.reduce((s, i) => s + i.price * i.count, 0) }},能用,但每次渲染都重算一遍,且模板可读性崩塌。换个思路放进 methods?也还能用,但方法调用没有缓存——模板里任何无关数据更新引起的重渲染,都会重新执行求和。

计算属性的价值正在这里:结果被缓存,依赖不变就不重算

// Vue 2 computed: { total() { console.log('total 重算了'); return this.items.reduce((s, i) => s + i.price * i.count, 0); } }

运行上面的例子,控制台只在首次渲染和 items 变化时打印"total 重算了";而把同样的逻辑放进 methods 并在模板里调用,任何无关更新(比如改一个时间戳字符串)都会让它重算。这个差异不是玄学,是机制。

二、缓存的机制解释

回到上一节的依赖模型。计算属性本质上是一个"计算 Watcher":

  1. 计算属性首次被读取时,它作为 activeWatcher 执行求值函数,期间读到的所有响应式属性(items 及其内部字段)都把这位计算 Watcher 登记进自己的 Dep;
  2. 求值结果存入缓存,标记 clean;
  3. 当 items 变化,通知这位计算 Watcher,它并不立刻重算,只是把自己标记为 dirty,同时通知依赖这个计算属性的渲染 Watcher;
  4. 下次渲染读取该计算属性时发现 dirty,才真正重算并更新缓存。

计算属性脏检查时序

也就是说缓存的失效是惰性的:没人读,脏了也不算。若某个计算属性在当前模板分支中已不被引用(v-if 切走了),它依赖的数据再怎么变,都不产生计算开销。

Vue 3 中用 ref 或 reactive 的 getter 表达同样的事:

import { computed, ref } from 'vue'; const items = ref([]); const total = computed(() => items.value.reduce((s, i) => s + i.price * i.count, 0) );

三、带 setter 的计算属性与一个反例

计算属性默认只有 getter,但 fullName 这类双向场景需要 setter:

computed: { fullName: { get() { return this.firstName + ' ' + this.lastName; }, set(val) { const parts = val.split(' '); this.firstName = parts[0]; this.lastName = parts[1] || ''; } } }

配合 v-model 即可实现"显示时拼合、输入时拆分"。

一个常见反例:用侦听器拼接 fullName。

// 反例:能用,但啰嗦且命令式 watch: { firstName(val) { this.fullName = val + ' ' + this.lastName; }, lastName(val) { this.fullName = this.firstName + ' ' + val; } }

两条侦听、两份拼接逻辑,还要处理初始值(得加 immediate)。派生值的需求用计算属性表达,代码量减半且不可能出现两条分支不同步的 bug。

四、侦听器:副作用的位置

什么时候轮到 watch?当数据变化要触发的是渲染之外的动作:发请求、操作 localStorage、开定时器、联动图表库。

watch: { keyword(newVal, oldVal) { // 典型副作用:搜索请求 this.debouncedSearch(newVal); } }

两个高频参数要会取舍:

  • immediate: true——创建时立即执行一次回调,省去手动初始化。适合"进页面就按当前条件请求一次"的场景;
  • deep: true——深度监听对象内部变化。代价是递归遍历整棵对象树,大对象上会拖慢更新,能用"换整个引用"或"精确监听某个字段"替代就别开 deep。

Vue 3 的写法更直观:

import { watch, watchEffect } from 'vue'; watch(keyword, (newVal, oldVal) => { debouncedSearch(newVal); }, { immediate: true }); // watchEffect:立即执行并自动收集回调里用到的依赖,无需指明监听目标 watchEffect(() => { document.title = keyword.value; });

三者选型可以压缩成一张决策表:

需求 选什么 理由
由现有数据算出新值 computed 缓存 + 声明式,杜绝不同步
数据变化后执行动作 watch 副作用与渲染解耦
模板里的简单格式化 表达式 三元、取反这个量级不值得抽出去
立即执行并自动追踪依赖 watchEffect 少写监听源,适合简单联动

五、nextTick:异步更新队列的用户接口

看一个真实困惑:

this.message = '新内容'; console.log(document.querySelector('#msg').textContent); // 还是旧内容!

原因在第 1 章末尾埋过伏笔:同一轮同步代码里的多次数据变更会被调度器合并,DOM 更新被推迟到当前微任务队列的末尾。想拿到更新后的 DOM,用 nextTick 把读取动作排到更新之后:

this.message = '新内容'; this.$nextTick(() => { console.log(document.querySelector('#msg').textContent); // 新内容 });

这个设计的收益是性能:循环里改一百次数据只触发一次渲染。代价是心智负担——"改完数据立刻读 DOM"这个直觉动作失效了。典型受害场景是"更新后聚焦输入框""根据内容高度算位置",这些都必须包进 nextTick。Vue 3 里对应 import { nextTick } from 'vue',机制完全相同。

本节要点回顾

  • 计算属性是缓存的派生值:脏标记惰性求值,依赖不变不重算,v-if 切走的分支连脏检查都省了;
  • 侦听器是副作用的挂点:immediate 补初始执行,deep 慎开,大对象优先换引用或精确监听;
  • 选型口诀:派生值用 computed,动作用 watch,简单格式化留在模板表达式;
  • nextTick 的必要性:批量异步更新换来了性能,也要求所有"改完就读 DOM"的代码排队;
  • Vue 3 增量:watchEffect 自动追踪依赖,是 computed 与 watch 之间的轻量补充。

数据层的第一站讲完了。但渲染函数从哪来?下一章往上游走:你写的模板字符串是怎么变成可执行代码的。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U