vue3中的watch可能是比watchEffect更好的选择
vue 最强大的功能之一是能够根据基本数据的变化来执行响应式副作用。为此,vue3提供两个方法:watch 和 watchEffect 。虽然两个方法都可以监控响应式数据的变化,但他们有不同的使用方式和行为。本文将探讨它们之间的差异以及何时使用它们。
watch
watch 方法允许我们观察一个或多个响应式数据源,并在有更新时调用回调函数。
语法虽然与vue2有所不同,但它的工作原理vue2相同的:
import { watch } from 'vue'
watch(source, callback, options?)
watch()方法接受3个参数:
-  source:响应式数据。响应式数据可能是从以下几种数据中获取到的:- getter方法或者- computed方法返回的值
- 一个ref数据
- 一个reactive数据
- 一个包含了上面任意数据的数组
 
-  callback:回调函数,当源数据更改时会调用的函数,该回调函数接受以下几个参数:
-  - value:观察数据的新状态
 
-  - oldValue:观察数据的旧状态
 
-  - onCleanup:一个可以用来注册清理回调的函数。清理回调将在下次重新运行效果之前调用,并可用于清理无效的副作用,例如一个未解决的异步请求。
 
-  options:可选配置,包含下列字段:
-  - immediate:一个布尔值,指示是否应该在观察器创建时立即触发回调,此时- oldValue是- undefined
 
-  - deep:一个布尔值,指示是否要执行源数据的深遍历。如果它是一个对象的话,那么回调就会启动深度监听。查看深度监听
 
-  - flush:表示如何调整回调时间的字符串。查看回调刷新时间
 
-  - onTrack / onTrigger:当观察器的依赖项被触发时调试它们的功能。查看观察者调试
 
watch() 功能默认是懒触发,这意味着只有在监听的源数据改变时才调用回调。
当监听多个源数据时,回调接收两个数组,其中包含与源数组对应的新旧值。
与watchEffect相比,watch 使我们能够:
- 懒触发副作用
- 更具体根据什么状态去定义、触发观察者重新运行
- 访问被监视状态的先前值和当前值。
示例
监听一个getter:
const state = reactive({ count: 0 })
watch(() => state.count,(count, prevCount) => {console.log(count, prevCount)}
)
监听一个ref:
const count = ref(0)
watch(count, (count, prevCount) => {console.log(count, prevCount)
})
当查看多个源时,回调接收到包含源数组相应的新旧值的数组:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {console.log("New values:" , foo, bar)console.log("Old values:" , prevFoo, prevBar)
})
WatchEffect
watchEffect 会立刻运行一个回调函数和自动追踪它的被动依赖关系。当任何反应依赖项发生变化时,都会执行回调函数。
import { reactive, watchEffect } from "vue"const state = reactive({count: 0,name: 'Leo'
})watchEffect(() => {// 立即执行// Count: 0, Name: Leoconsole.log(`Count: ${state.count}, Name: ${state.name}`)
})state.count++ // Count: 1, Name: Leo
state.name = 'alex' // Count: 1, Name: alex
在上面的例子中,我们使用watchEffect来监听状态、计数和名称属性的更改。每当它们中的任何一个更改回调函数都会记录当前的计数值和名称值。
第一次watchEffect 调用时,立即执行回调函数,使用当前的计数和名称值。在此之后,当任何反应依赖项(计数或名称)更改时,将重新运行回调函数。
就像watch一样,watchEffect 也有一些额外的功能使它更加强大。我们可以通过一个选项对象作为配置观察者行为的第二个参数。例如,可以指定flush timing  (当观察者被执行时) 或者添加一个调试钩子。
回调函数的第一个参数为onCleanup 这个特殊的函数时,可以使用此函数来注册将在重新执行监视器之前调用的清理回调。这有助于清理不再需要的资源。
import { ref, watchEffect } from "vue"const id = ref(1)
const data = ref(null)watchEffect(async (onCleanup) => {const { response, cancel } = await fetch(`https://example.com/api/data/${id.value}`)onCleanup(cancel)data.value = response.data
})
在上面的例子中,我们使用watchEffect 在ID属性更改时从API中获取数据。我们使用onCleanup 功能注册一个取消功能,如果ID属性在请求完成之前更改,该功能将取消获取请求。
另外,我们可以使用watchEffect 去阻止监听。
import { watchEffect } from "vue"const stop = watchEffect(() => {// …
})
// Stop the watcher
stop()
另一个令人困惑的问题是watchEffect 仅在同步执行期间跟踪依赖关系。在第一个等待项之前访问的所有属性将在使用一个异步回调时被跟踪,但之后的所有属性不会被跟踪。
<script setup>
import { reactive, watchEffect } from "vue"const state = reactive({count: 0,name: 'Leo'
})const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));watchEffect(async () => {console.log(`Count: ${state.count}`) // This will be trackedawait sleep(200)console.log(`Name:  ${state.name}`)  // This will NOT be tracked
})</script><template>
<input type="number" v-model="state.count" />
<br />
<input v-model="state.name" />
</template>
await 之前的一切(即state.count )将被追踪。
 await 之后的一切(即state.name )不被追踪。
总结
watch 和watchEffect 两者都允许我们以响应式方式执行副作用,但它们在如何跟踪依赖性方面有所不同:
- watch只追踪显式监视源,不会跟踪回调中的任何访问。此外,回调只在源实际发生更改时触发。把依赖追踪和副作用分开,当回调运行时- watch提供更精确的控制。
- watchEffect将依赖追踪和副作用结合到一个阶段。在同步执行期间,它自动跟踪所访问的每个响应式属性。这将导致更简洁的代码,但使得它的被动依赖性不那么明确。
如果我们需要查看嵌套数据结构中的几个属性,watchEffect() 可能比深度监视器更有效,因为它只跟踪回调中使用的属性,而不是递归跟踪所有的属性。
但watch能确保更好的总体控制,区分依赖跟踪和副作用,避免意外造成性能下降。