watch
和watchEffect
是Vue 3.0中新增的两个响应式API,用于监听数据的变化。watch
适用于需要获取新值和旧值,或者需要懒执行的场景,而watchEffect
适用于需要监听多个数据源,并且需要立即执行的场景。它们之间的区别如下:
watch
是监听单个数据源,可以设置immediate
和deep
选项,可以获取新值和旧值;watchEffect
则是监听一组数据源,不能设置immediate
和deep
选项,不能获取新值和旧值。watch
是懒执行的,只有在数据变化时才会执行回调函数,而watchEffect
则是立即执行的,不管数据是否变化。watch
可以监听计算属性,而watchEffect
不能监听计算属性。
watch和watchEffect用法区别
watch
和watchEffect
在使用上也有一些区别,具体如下:
watch
的使用方法:
watch(source, callback, options)
其中,
source
可以是一个响应式数据源,也可以是一个函数,返回一个响应式数据源。callback
是回调函数,会在数据变化时执行,可以获取新值和旧值。options
是一个对象,可以设置immediate
和deep
等选项。
watch使用示例:
import { ref, watch} from 'vue'const count = ref(0)// 使用watch监听数据变化,并在下一次DOM更新前执行回调函数
watch(count, (newVal, oldVal) => {console.log('count变化了', newVal, oldVal)
}, {flush: 'pre'
})// 修改数据
count.value++
watchEffect
的使用方法:
watchEffect(callback, options)
其中,
callback
是一个函数,会在组件渲染时执行,并且会在数据变化时重新执行。options
是一个对象,可以设置flush
选项。需要注意的是,
watchEffect
的回调函数中可以直接使用响应式数据源,不需要显式获取新值和旧值,因为watchEffect
会自动收集依赖,在数据变化时重新执行回调函数。
watchEffect示例
import { ref, watchEffect } from 'vue'const count = ref(0)// 使用watchEffect监听数据变化,默认在下一次DOM更新后执行回调函数
watchEffect(() => {console.log('count2变化了', count.value)
})// 修改数据
count.value++
源码分析
watch和watchEffect相关源码都在core-main\packages\runtime-core\src\apiWatch.ts文件中
watchEffect方法
在源码里,watchEffect是一个函数,接收两个参数,第一个是effect,也就是用户传入的执行函数。第二个是options,设置flush属性,flush属性表示回调函数的执行时机。默认
flush
选项是pre
,即在依赖变化之后立即执行副作用函数。
watchEffect内部调用了doWatch方法,doWatch方法接收三个参数,watchEffect在调用的doWatch时候将第二个参数设为null 。这里是重点,为什么,因为等下看watch实现的时候就知道了,watch也调用了doWatch方法。只不过第二个参数不为null了。watchEffect先看到这,你就记住要去调doWatch方法。接下来去看doWatch怎么实现的。
flush具体有以下几个选项:
'pre'
:在下一次 DOM 更新前执行回调函数(默认选项)。'post'
:在下一次 DOM 更新后执行回调函数。'sync'
:立即执行回调函数。
watchPostEffect和watchSyncEffect 方法
值得注意的是,这个watchEffect方法下面,还对外抛出了两个方法,watchPostEffect和watchSyncEffect。前面说了通过flush属性控制effect执行的时机,watchEffect默认是flush:pre,DOM 更新前执行。如果你想改变flush,除了在watchEffect改变flush的值,还可以直接调watchPostEffect和watchSyncEffect方法。
watch方法
在源码里,watch是一个函数,接收三个参数,第一个是source,也就是用户的要监听的属性。第二个是回调函数,通常使用箭头函数,监听新旧值的变化并执行内部的方法。第三个是options,可以设置flush\immediate\deep\once属性。默认
flush
选项也是pre
,即在依赖变化之后立即执行副作用函数。
watch内部也调用了doWatch方法,doWatch方法接收watch传入的三个参数。
核心doWatch方法
从前面的分析可以看到,watchEffect和watch都执行了doWatch方法。唯一不同的是watchEffect使用doWatch时第二个参数使用的是null。那么看下doWatch的执行逻辑吧
doWatch接收的参数
doWatch方法接收三个参数:source、cb、options。具体解释如下:
souce:要监听的数据源,可以是一个
ref
对象、一个reactive
对象、一个数组或一个函数。cb:是回调函数,在watch中使用。(newValue,oldValue)=>{}。
options:是一个配置对象,接收以下属性
deep
:是否深度监听,默认值为false
。如果设置为true
,则会递归地监听source
对象的所有属性和子属性。immediate
:是否立即执行回调函数,默认值为false
。如果设置为true
,则会在监听器被创建时立即执行一次回调函ctions。flush
:指定回调函数的刷新时机,可以是'pre'
、'post'
或'sync'
,默认值为'pre'
。onTrack
:一个函数,用于在追踪依赖时调用。onTrigger
:一个函数,用于在触发依赖时调用。
getter 函数——获取监听数据执行结果
doWatch方法第一个核心是定义一个getter函数,用于获取需要监听的数据。
getter
的值是根据source
的不同类型而得到。
- 如果
source
是一个ref
对象,那么getter
返回source.value
。 - 如果
source
是一个reactive
对象,那么getter
返回reactiveGetter(source)
。 - 如果
source
是一个数组,那么getter
返回一个数组,其中每个元素是一个值或一个函数的执行结果。具体来说,如果数组中的某个元素是一个ref
对象,那么返回s.value
,如果是一个reactive
对象,那么返回reactiveGetter(s)
,如果是一个函数,那么返回callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
。 - 如果
source
是一个函数,那么getter
的值取决于cb
的值:- 如果
cb
存在,那么getter
的值是一个函数,返回callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
。 - 否则没有cb,
getter
的值是一个函数,返回一个值或一个函数的执行结果。(也就是watchEffect)具体来说,如果cleanup
存在,那么首先执行cleanup()
,然后返回callWithAsyncErrorHandling(source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup])
。
- 如果
effect——响应式数据依赖管理
doWatch 的第二个关键是通过new Reactive()创建一个effect实例。接收getter、NOOP、scheduler。getter是一个函数,根据source不同返回一个值或一个函数的执行结果。NOOP是一个空的箭头函数。scheduler是任务job的调度器,默认flush:pre走的是queueJob任务队列调度器,通过队列管理任务,先进先出。
这个effect是什么?scheduler是什么?queueJob(job)又是什么呢?接着看源码
ReactiveEffect作用——依赖收集
这个ReactiveEffect是实现vue3响应式数据更新的核心类。当数据变化时,可以通过
trigger
函数触发所有依赖该数据的效果,从而更新相关的视图。reactiveEffect
类的作用包括:
- 构造函数:接受四个参数,分别是用于执行的函数
fn
、触发更新的函数trigger
、可选的调度器函数scheduler
和可选的效果作用域scope
。dirty
属性:一个只读属性,表示当前的脏标记,可以是DirtyLevels.NotDirty
、DirtyLevels.Dirty
、DirtyLevels.MaybeDirty
或DirtyLevels.QueryingDirty
之一。run
方法:执行当前效果,即调用fn
函数,并在执行前后进行一些必要的操作,如更新脏标记、记录活跃的效果等。stop
方法:停止当前效果,即取消对fn
函数的依赖,并进行一些必要的清理操作。
调度器scheduler——管理job的工具
Vue 3 中的
scheduler.ts
文件是用来实现调度器(scheduler)功能的。调度器在 Vue 中负责管理异步更新的调度和执行,确保在适当的时机更新组件以及处理副作用函数。具体来说,scheduler.ts
文件包含了以下主要功能:
-
调度更新:调度器负责管理组件的更新调度,根据更新的优先级和策略来决定何时执行更新操作,以提高性能和效率。
-
异步更新:调度器可以将更新操作异步执行,通过微任务或宏任务来延迟更新操作,以避免阻塞主线程,提高页面的响应性。
-
副作用函数的调度:调度器还负责管理副作用函数的执行时机,根据副作用函数的依赖关系和执行策略来调度副作用函数的执行。
-
性能优化:调度器可以根据具体情况对更新操作进行优化,比如批量更新、合并更新等操作,以减少不必要的更新和提高性能。
queueJob
函数来判断当前任务是否已存在于任务队列中,如果不存在,则将当前任务插入到任务队列中。当任务队列中的任务数量达到一定的阈值时,scheduler
函数会通过queueFlush
函数来执行任务队列中的任务。
watch和watchEffect的核心区别——job函数做了什么
前面经过分析,doWatch拿到了三个参数(source,cb,options);然后根据source的不同,定义了一个getter函数,得到了监听数据的执行结果。然后又定义了一个effect管理依赖收集,通过effect的dirty判断依赖的属性有没有变化,effect的run方法可以运行接收的第一个fn函数,在这里fn是getter函数。effect还有一个调度器函数,默认是queueJob(job)。queueJob用来管理一个任务队列。核心方法是看单个job是怎么实现的。
job的定义还是在doWatch方法内,可以看到,job先是对effect的active和dirty进行判断,如果都不满足,不需要重新执行方法。
接着判断cb,也就是doWatch接收的第二个参数。还记得吗,watch调用doWatch的时候第二个参数是回调函数(newValue,oldValue)=>{},而watchEffect调用的时候第二个参数传的是null。在这里可以看到watch和watchEffect的区别了!
如果cb存在,先去执行effect.run()方法,得到返回值给newValue,然后将newValue和oldValue拿到,执行回调方法cb
如果cb不存在,直接执行effect的run方法
可以看到,watchEffect就少了一步,就是它不需要去处理新值和旧值的逻辑;如果在依赖的数据发生变化的时候,你必须想知道是哪个数据变化,并且旧值和新值的比较关系,那么你就用watch,如果你不关心旧值,也不关心是哪个数据项变化,你就用watchEffect。在doWatch里会将watchEffect的第一个参数当成getter,在effect执行run的时候去执行getter,也就是你传入watchEffect的函数。
总结
在这部分源码分析中,用了很长的篇幅取介绍doWatch的实现,因为watch和watchEffect都调用了doWatch,只是第二个参数不同,watchEffect传入的是null,watch传入的是cb。
doWatch干了什么事情呢?
首先由要收集数据依赖,收集的是哪写属性呢?
source传入的可能是ref\reactive属性、数组或者函数,这个时候需要对source进行计算,看最终依赖的是什么东西?通过定义一个getter函数,根据source类型不同,得到监听数据最后的执行结果。
怎么收集数据依赖呢?
使用ReactiveEffect类,定义一个effect实例,将前面定义的getter传进去,通过操作effect的run方法执行getter,通过effect的dirty属性判断getter是否改变。
数据改变了,doWatch怎么做呢?
数据发生变化了,也就是effect的dirty属性或者active属性有一个为true了。这时doWatch根据传入的第二个参数cb是否有值进行判断。
如果cb有值,那就是watch方法,watch是不是想知道依赖的getter发生变化后得到的新值和之前的旧值啊,所以cb有值的时候先去执行effect的run方法得到依赖属性变化后的新值newValue,将newValue和oldValue传入cb执行cb回调方法。
如果cb没值,那就是watchEffect方法。直接去执行effect的run方法,将传入的source转换为getter,执行就好了。
总的来说 watch
vs watchEffect
watch
用于需要知道是哪个数据项发生变化以及需要比较旧值和新值的情况,因为它提供了旧值和新值的参数。而watchEffect
则更适合在不关心旧值和具体数据项变化的情况下使用,因为它只关注副作用函数的执行,不提供旧值和新值的参数。
看到这里,如果你已经稍微理解watch和watchEffect的区别了,请一键三连哦~