为啥面试官总喜欢问computed是咋实现的?

前言

通过前面几篇文章,我们对Vue3中的响应式设计有了初步的了解。

  1. 面试官:Vue3响应式系统都不会写,还敢说精通?

  2. 面试官:你觉得Vue的响应式系统仅仅是一个Proxy?

  3. Vue3:原来你是这样的“异步更新”

而对于每天都在用的计算属性(computed),我猜你肯定也想窥探其奥妙与原理对吧!走起!!!

从computed的特性出发

computed最耀眼的几个特性是啥?

1. 依赖追踪

import { reactive, computed } from 'vue'

const state = reactive({
  a: 1,
  b: 2,
  c: 3,
})
const sum = computed(() => {
  return state.a + state.b
})

我们定义了一个响应式数据state和一个计算属性sum, Vue会自动追踪sum依赖的数据state.astate.b,并建立相应的依赖关系。

也就是只有state.astate.b发生变化的时候,sum才会重新计算而state.c任由它怎么变,sum都将丝毫不受影响。

2. 缓存

还是上面的例子,如果state.astate.b打死都不再改变值了,那么我们读取sum的时候,它将会返回上一次计算的结果,而不是重新计算。

3. 懒计算

这个特性比较容易被忽略,简单地说只有计算属性真正被使用(读取)的时候才会进行计算,否则咱就仅仅是定义了一个变量而已。

import { reactive, computed } from 'vue'

const state = reactive({
  a: 1,
  b: 2,
  c: 3
})

const sum = computed(() => {
  console.log('执行计算')
  return state.a + state.b
})

setTimeout(() => {
  // 没有读取sum.value之前,sum不会进行计算
  console.log('1-sum', sum.value)
  // 我们改变了a的值,但是sum并不会立刻进行计算
  state.a = 4

  setTimeout(() => {
    // 而是要等到再次读取的时候才会触发重新计算
    console.log('2-sum', sum.value)
  }, 1000)
}, 1000)
1c3ed62b9011941df75771f1197fbdba.png

挨个实现computed特性

1. 懒计算

我们依旧围绕effect函数来搞事情,到目前为止,effect注册的回调都是立刻执行。

const state = reactive({
  a: 1,
  b: 2,
  c: 3
})
// 有没有很像计算属性的感觉
const sum = effect(() => {
  console.log('执行计算') // 立刻被打印
  const value = state.a + state.b
  return value
})

console.log(sum) // undefined

想要实现computed的懒执行,咱们可以参考上篇文章Vue3:原来你是这样的“异步更新”的思路,添加一个额外的参数lazy

它要实现的功能是:如果传递了lazytrue,副作用函数将不会立即执行,而是将执行的时机交还给用户,由用户决定啥时候执行。

当然啦!回调的结果我们也应该一并返回(例如上面的value值)

你能想象,我们仅仅需要改造几行代码就能离computed近了一大步。

const effect = function (fn, options = {}) {
  const effectFn = () => {
    // ... 省略
    // 新增res存储fn执行的结果
    const res = fn()
    // ... 省略
    // 新增返回结果
    return res
  }
  // ... 省略
  // 新增,只有lazy不为true时才会立即执行
  if (!options.lazy) {
    effectFn()
  }
  // 新增,返回副作用函数让用户执行
  return effectFn
}

测试一波

const state = reactive({
  a: 1,
  b: 2,
  c: 3,
});
// 有没有很像计算属性的感觉
const sum = effect(() => {
  console.log("执行计算"); // 调用sum函数后被打印
  const value = state.a + state.b;
  return value;
}, {
  lazy: true
});
// 不执行sum函数,effect注册的回调将不会执行
console.log(sum()); // 3

2. 依赖追踪

咱们初步实现了懒执行的特性,为了更像computed一点,我们需要封装一个函数。

function computed (getter) {
  const effectFn = effect(getter, {
    lazy: true,
  })

  const obj = {
    get value () {
      return effectFn()
    }
  }

  return obj
}

这就有点那么味道啦!

测试一波

可以看到computed只会依赖state.astate.b,而不会依赖state.c,这得益于我们前面几篇文章实现的响应式系统,所以到了计算属性这里,我们不用改动任何代码,天然就支持。

不过还是有点小问题,我们读取了两次sum.value,sum却被执行了两次,这和computed缓存的特性就不符了。

别急,马上就要实现了这个最重要的特性了。

const state = reactive({
  a: 1,
  b: 2,
  c: 3
})

const sum = computed(() => {
  console.log('执行计算')
  return state.a + state.b
})

console.log(sum.value)
console.log(sum.value)
c663a9f3091ee9df26401374aebf5480.png

3. 缓存

回顾一下computed的缓存特性:

  1. 只有当其依赖的东西发生变化了才需要重新计算

  2. 否则就返回上一次执行的结果。

为了缓存上一次计算的结果,咱们需要定义一个value变量,现在的关键是怎么才能知道其依赖的数据发生变化了呢?

function computed (getter) {
  const effectFn = effect(getter, {
    lazy: true,
  })
  let value
  let dirty = true

  const obj = {
    get value () {
      // 2. 只有数据发生变化了才去重新计算
      if (dirty) {
        value = effectFn()
        dirty = false
      }

      return value
    }
  }

  return obj
}

测试一波

const state = reactive({
  a: 1,
  b: 2,
  c: 3
})

const sum = computed(() => {
  console.log('执行计算')
  return state.a + state.b
})

console.log(sum.value) // 3
console.log(sum.value) // 3

state.a = 4

console.log(sum.value) // 3 答案是错误的

寄上任务调度

不得不说,任务调度实在太强大了,不仅仅可以实现数组的异步批量更新、在computedwatch中也是必不可少的。

function computed (getter) {
  const effectFn = effect(getter, {
    lazy: true,
    // 数据发生变化后,不执行注册的回调,而是执行scheduler
    scheduler () {
      // 数据发生了变化后,则重新设置为dirty,那么下次就会重新计算
      dirty = true
    }
  })
  let value
  let dirty = true

  const obj = {
    get value () {
      // 2. 只有数据发生变化了才去重新计算
      if (dirty) {
        value = effectFn()
        dirty = false
      }

      return value
    }
  }

  return obj
}

测试一波

const state = reactive({
  a: 1,
  b: 2,
  c: 3
})

const sum = computed(() => {
  console.log('执行计算')
  return state.a + state.b
})

console.log(sum.value) // 3
console.log(sum.value) // 3

state.a = 4

console.log(sum.value) // 3 答案是错误的

完美!!!这下面试官再也难不倒我了!!!

1fe8ea66c620191a7062f5c010d70124.png

结尾

最近在阅读霍春阳大佬的 《Vue.js技术设计与实现》,本文的内容主要来源于这本书,强烈推荐对Vue底层实现感兴趣的同学阅读。

<think>好的,我现在需要回答用户关于Vue中watch和computed区别的题。首先,我得回想一下自己对这两个概念的理解,确保准确无误。 用户之前了关于data为什么是函数的题,现在接着第二个题,可能是在深入学习Vue的响应式系统。我需要明确区分watch和computed,避免混淆。 首先,计算属性computed。它们是基于它们的依赖进行缓存的,只有在依赖发生变化时才会重新计算。这意味着如果依赖没有变化,多次访计算属性会立即返回之前的计算结果,而不必再次执行函数。这适合处理需要复杂计算或频繁访的数据,比如对大量数据进行过滤或排序。 然后是侦听器watch。它们用于观察和响应数据的变化,当某个数据变化时执行异步操作或开销较大的操作。例如,当用户输入搜索关键词时,可能需要发送API请求,这时候用watch更合适,因为它可以在数据变化时执行特定操作,而计算属性则不适合处理异步任务。 接下来需要明确两者的区别。首先是缓存机制,computed有缓存,而watch没有。然后是使用场景,computed适合计算派生数据,而watch适合执行副作用。另外,computed是声明式的,而watch更命令式,需要自己处理变化后的逻辑。 还要考虑语法上的不同。computed定义的是函数,返回计算结果;watch则需要指定要侦听的属性,并定义处理函数。此外,computed默认是立即求值的,而watch可能需要设置immediate选项来立即执行回调。 可能需要举例子来帮助理解。比如,全名可以由firstName和lastName组合而成,适合用computed;而如果需要在某个数据变化时保存到本地存储,适合用watch。 最后要确保回答结构清晰,分点说明区别,并给出使用场景的建议。这样用户能够逐步理解两者的不同,并根据实际需求选择合适的方式。</think>在 Vue 中,`watch` 和 `computed` 都是响应式系统的重要组成部分,但它们的用途和实现方式有本质区别。以下是二者的核心差异: --- ### 一、**核心区别** | | **Computed (计算属性)** | **Watch (侦听器)** | |---------------|------------------------------------------------|--------------------------------------------| | **定位** | **声明式**<br>(描述"应该是什么") | **命令式**<br>(描述"当变化时做什么") | | **缓存机制** | 自动缓存结果(依赖不变时直接返回缓存值) | 无缓存(每次变化都执行回调) | | **异步支持** | ❌ 必须是同步操作 | ✅ 可执行异步操作(如 API 请求) | | **返回值** | 必须返回一个值(通过函数返回值) | 无返回值(通过回调参数操作副作用) | --- ### 二、**使用场景对比** #### 1. **Computed 的典型场景** - **数据派生**:需要根据其他响应式数据 **组合/计算** 新值 ```javascript computed: { fullName() { return this.firstName + ' ' + this.lastName } } ``` - **性能优化**:当依赖未变化时,直接复用缓存结果 ```javascript computed: { filteredList() { return this.list.filter(item => item.price > 100) } } ``` #### 2. **Watch 的典型场景** - **副作用操作**:当数据变化时需要 **触发异步操作** 或 **复杂逻辑** ```javascript watch: { searchQuery(newVal) { this.fetchData(newVal) // 发送 API 请求 } } ``` - **深度监听**:检测对象/数组内部变化(需设置 `deep: true`) ```javascript watch: { user: { handler(newVal) { /* 处理变化 */ }, deep: true } } ``` --- ### 三、**底层原理差异** | | Computed | Watch | |------------------|-------------------------------------------|-------------------------------------------| | **依赖追踪** | 自动追踪所有依赖的响应式属性 | 需显式指定要侦听的特定属性 | | **触发时机** | 依赖变化后 **立即计算**(同步) | 默认在依赖变化后 **延迟触发**(可通过选项配置) | | **关联性** | 与模板绑定更紧密(通常直接用于渲染) | 与业务逻辑绑定更紧密(处理数据变化的副作用) | --- ### 四、**选择建议** - **优先使用 `computed`**:当需要 **基于现有状态计算新值** 且 **无需副作用** 时 (如格式化日期、过滤列表、数学运算等) - **必须使用 `watch`**:当需要 **响应数据变化执行异步操作** 或 **复杂逻辑** 时 (如保存数据到 localStorage、发送请求、操作 DOM 等) --- ### 示例对比 ```javascript // Computed:组合数据 computed: { totalPrice() { return this.items.reduce((sum, item) => sum + item.price, 0) } } // Watch:监听价格变化并保存 watch: { totalPrice(newVal) { localStorage.setItem('total', newVal) } } ``` --- ### 结 `computed` 是 **数据驱动** 的声明式工具,而 `watch` 是 **事件驱动** 的命令式工具,理解它们的差异能帮助你更精准地选择响应式方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值