『引言』
watch更多的是观察的作用,无缓存性, 类似于某些数据的监听回调 ,每当监听的数据变化时,都会执行回调进行后续操作。
『回顾』
通过一个简单示例回忆一下,vue2中watch的使用。
『示例』
xml
<template>
<div>
<div>Hello,大家好,我的名字叫:{{ info.name }}</div>
<div>我的爱好有:{{ info.hobbies.join('、') }}</div>
<div>很高兴认识大家!</div>
<button @click="changeName">点击按钮修改信息</button>
</div>
</template>
<script>
export default {
data() {
return {
info:{
name: 'pupu',
hobbies: ['唱歌', '画画']
}
}
},
methods: {
changeName() {
this.info.name = 'wnxx'
this.info.hobbies = ['打羽毛球', '旅游']
}
},
watch: {
'info.name': {
handler(newVal,oldVal){
console.log('原来的值是:' + oldVal)
console.log('改后的值是:' + newVal)
},
immediate: true,
deep: true
},
'info.hobbies': {
handler(newVal,oldVal){
console.log('原来的值是:' + oldVal)
console.log('改后的值是:' + newVal)
},
immediate: true,
deep: true
},
}
}
</script>
『效果展示』
『watch()』
『定义』
【官方解释】
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
-
详细信息
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。
与
watchEffect()
相比,watch()
使我们可以:- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
『用法』
watch(WatcherSource, Callback, [WatchOptions])
wacth(value,(newValue,oldValue)=>{}[,options])
一次监听多个响应式的值: wacth([value1,value2],([newValue1,newValue2],[oldValue1,oldvalue2])=>{}[,options])
『watch参数说明』:
- WatcherSource: 想要监听的响应式数据。
- Callback: 回调函数,参数(newValue, oldValue)。
- [WatchOptions]:deep、immediate、flush可选。
『[WatchOptions]参数说明』:
- deep:当需要对对象等引用类型数据进行深度监听时,设置deep: true,默认值是false。
- immediate:默认是false,初始化的时候不执行回调函数。如果是true,watch会初始化的时候就会执行一次回调函数。
- flush:控制回调函数的执行时机,。它可设置为 pre、post 或 sync。
- pre:默认值,当监听的值发生变更时,优先执行回调函数(在dom更新之前执行)。
- post:dom更新渲染完毕后,执行回调函数。
- sync:一旦监听的值发生了变化,同步执行回调函数(建议少用)。
『官网示例🌰』
『注意』
watch在可配置为空时:
- 运行的时候,不会立即执行。
- 需要添加监听的属性。
- 回调函数内会返回最新值和修改之前的值。
- 配置项可补充 watch 特点上的不足。
『示例🌰:监听ref定义的数据』
xml
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{name}}</p>
<p>年龄:{{age}}岁</p>
<p>爱好:{{info.hobbies.join('、')}}</p>
<p>地址:{{info.address.provice}} - {{info.address.city}}</p>
<p>最喜欢的颜色:{{info.favoriteColor.coloeOne}} & {{ info.favoriteColor.colorTwo }}</p>
<p>描述:{{info.description}}</p>
<button @click="modifyInfo">
修改数据
</button>
</div>
</template>
<script setup>
import { ref,watch } from 'vue'
const name = ref('pupu')
const age = ref(10)
const info = ref({
hobbies: ['唱歌','画画'],
address: {
provice: '浙江省',
city: '杭州市'
},
favoriteColor: {
coloeOne: '绿色',
colorTwo: '蓝色'
},
description: '一点也不可爱,不喜欢吃蜂蜜!',
})
const modifyInfo = () => {
name.value = 'wnxx'
age.value = 3
info.value.hobbies = ['打羽毛球','旅游']
info.value.address.provice = '云南省'
info.value.address.city = '丽江市'
info.value.description = '非常的可爱,特别喜欢吃蜂蜜!'
info.value.favoriteColor.coloeOne = '薄荷绿'
info.value.favoriteColor.colorTwo = '天蓝色'
}
// 情况1:监听ref所定义的一个响应式数据
watch(name, (newValue,oldValue) => {
console.log(newValue, oldValue, 'name' )
}),
// 情况2:监听ref所定义的多个响应式数据
watch([name,age],(newValue,oldValue) => {
console.log( newValue, oldValue, 'name-age')
}),
// 情况3:监听ref所定义的一个引用类型响应式数据
watch(info, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info引用类型' )
}),
// 情况4:针对监听ref所定义的一个引用类型响应式数据之一
watch(info.value, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info.value' )
}),
// 情况5:针对监听ref所定义的一个引用类型响应式数据之二
watch(info.value, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info深度监听无效' )
},
{deep: false}
),
// 情况6:针对监听ref所定义的一个引用类型数据之三:直接深度监听
watch(info, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info直接深度监听' )
},
{deep: true}
)
// 情况7:针对监听ref所定义的一个引用类型数据之四,深拷贝深度监听
watch(() => ({...info.value}), (newValue,oldValue) => {
console.log(newValue, oldValue, 'info深拷贝深度监听' )
},
{deep: true}
)
</script>
『效果展示』
『代码解析』
上述代码中列举了几种使用ref
监听不同数据的方式和情况。
-
情况1:监听
ref
所定义的一个
响应式数据,可以获取到新旧值。 -
情况2:监听
ref
所定义的多个
响应式数据,可以获取到新旧值,多个参数是数组,数组形式返回。 -
情况3:监听
ref
所定义的一个引用类型
响应式数据,不会获取到新旧值。监听引用类型数据的内部某一项发生变化,不会被监听到,watch中的代码不会执行,控制台也并未看到【'info引用类型'】
的打印信息。 -
情况4:针对监听
ref
所定义的一个引用类型响应式数据之一,使用【info.value】
,可以获取到新值。【info.value】
其实是一个对象的引用地址,新旧值
拿到的值是一样的。 -
情况5:针对监听
ref
所定义的一个引用类型响应式数据之二。如果写了.value
那么它对Proxy的监听默认是深度监听,且无法关闭,配置deep:false无效
。控制台打印信息和情况4一样。 -
情况6:针对监听
ref
所定义的一个引用类型数据之三:直接深度监听。如果不写.value
,写上deep:true
,实现深度监听。但是深度监听的是这个引用数据类型自身,而不是其中的属性。这样只能获取到新值,而获取不到旧的值。 -
情况7:针对监听
ref
所定义的一个引用类型数据之四,深拷贝深度监听。通过监听数据的getter函数,这样『newValue和oldValue』
得到的对象是正确的。
提问 🚩:首先看到ref
,就会想到模板语法中不用value,js中操作数据需要带上value
。那为什么watch的时候没有写value
❓🤔🤔
答案 📒:首先ref
定义的数据,得到的是一个RefImpt:引入对象的实现对象的实例对象,简称引用对象。
然后,监听ref
定义的基本类型的数据,不需要value
。
最后,监听ref
定义的对象类型的数据,需要value
,因为监听对象时,要对这个对象的Proxy对象
生效,对象类型的数据经过ref函数
加工会变成引用对象,而该对象的value
属性才是这个对象的Proxy
。所以如果需要监视Proxy对象中的数据
则需要监视的是.value
的结构。
『示例🌰:监听reactive定义的数据』
xml
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{person.name}}</p>
<p>年龄:{{person.age}}岁</p>
<p>爱好:{{info.hobbies.join('、')}}</p>
<p>地址:{{info.address.provice}} - {{info.address.city}}</p>
<p>最喜欢的颜色:{{info.favoriteColor.coloeOne}} & {{ info.favoriteColor.colorTwo }}</p>
<p>描述:{{info.description}}</p>
<button @click="modifyInfo">
修改数据
</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const person = reactive({
name: 'pupu',
age: 10
})
const info = reactive({
hobbies: ['唱歌','画画'],
address: {
provice: '浙江省',
city: '杭州市'
},
favoriteColor: {
coloeOne: '绿色',
colorTwo: '蓝色'
},
description: '一点也不可爱,不喜欢吃蜂蜜!',
})
const modifyInfo = () => {
person.name = 'wnxx'
person.age = 3
info.hobbies = ['打羽毛球','旅游']
info.address.provice = '云南省'
info.address.city = '丽江市'
info.favoriteColor.coloeOne = '薄荷绿'
info.favoriteColor.colorTwo = '天蓝色'
info.description = '非常的可爱,特别喜欢吃蜂蜜!'
}
// 情况1:监听reactive所定义的一个响应式数据中的某个属性
watch(()=>person.name, (newValue,oldValue) => {
console.log(newValue, oldValue, 'name' )
}),
// 情况2:监听reactive所定义的一个响应式数据中的某些属性
watch([()=>person.name,()=>person.age],(newValue,oldValue) => {
console.log( newValue, oldValue, 'name-age')
}),
// 情况3:监听reactive所定义的引用类型响应式数据的某个属性
watch(()=>info.hobbies, (newValue,oldValue) => {
console.log(newValue, oldValue, '爱好' )
}),
// 情况4:监听reactive所定义的引用类型响应式数据的某些属性
watch([()=>info.hobbies,()=>info.description], ([newHobby,newDescription],[oldHobby,oldDescription]) => {
console.log(newHobby,newDescription,oldHobby,oldDescription, '爱好描述' )
})
// 情况5:针对监听reactive所定义的引用类型响应式数据的全部属性之一
watch(info, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info引用类型' )
}),
// 情况6:针对监听reactive所定义的引用类型响应式数据的全部属性之二
watch(() =>info, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info引用类型之二' )
},
{deep: true}
),
// 情况7:只监听reactive所定义的引用类型响应式数据的子属性
watch(() => ({...info}), (newValue,oldValue) => {
console.log(newValue, oldValue, 'info子属性' )
})
// 情况8:监听reactive所定义的引用类型响应式数据里的某个对象的属性
watch(() => info.address, (newValue,oldValue) => {
console.log(newValue, oldValue, 'info中的地址' )
},
{deep: true}
)
</script>
『效果展示』
『代码解析』
上述代码中列举了几种使用reactive
监听不同数据的方式和情况。
-
情况1:监听
reactive
所定义的一个
响应式数据,可以获取到新旧值。第一个参数写成函数的形式才会生效。 -
情况2:监听
reactive
所定义的多个
响应式数据,可以获取到新旧值,第一个参数写成数组的形式,返回也是数组形式。 -
情况3:监听
reactive
所定义的引用类型响应式数据的某个属性,可以获取到新旧值。 -
情况4:监听
reactive
所定义的引用类型响应式数据的某些属性,可以获取到新旧值。 -
情况5:针对监听
reactive
所定义的引用类型响应式数据的全部属性之一,只可以获取到新值,对象里面任何一个属性改变都会触发。 -
情况6:针对监听
reactive
所定义的引用类型响应式数据的全部属性之二,添加{deep:true}
,对象里的属性都会监听到。 -
情况7:只监听
reactive
所定义的引用类型响应式数据的子属性,可以获取到新旧值。 -
情况8:监听reactive所定义的引用类型响应式数据里的某个对象的属性,只能获取到新值,需要添加
{deep:true}
,监听才有效。
『watch源码』
『watch源码』
watch源码如下⬇️:
r
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
return doWatch(source as any, cb, options)
}
『参数说明』
-
source: 监听的数据源
source 的类型如下:
typescript
export type WatchSource<T = any> = Ref<T> | ComputedRef<T>
| (() => T) type MultiWatchSources =
(WatchSource<unknown>| object)[]
可以清晰的知道,数据源支持传入『单个的Ref、Computed
响应式对象、一个返回相同泛型类型的函数』,以及source
支持传入数组,方便能同时监听多个数据源。
-
cb: 回调函数
cb的类型如下:
typescript
export type WatchCallback<V = any, OV = any>
= ( value: V, oldValue: OV, onInvalidate:
InvalidateCbRegistrator ) => any
回调函数提供最新的value
、旧的value
,以及onInvalidate函数
用以清除副作用。
-
options: 监听配置
options的类型如下:
typescript
export interface WatchOptions<Immediate = boolean>
extends WatchOptionsBase
{ immediate?: Immediate deep?: boolean }
监听配置的参数有immediate
和 deep
,监听配置的类型 WatchOptions
继承了 WatchOptionsBase
,可以传递 WatchOptionsBase
中的所有参数以控制副作用执行的行为。
『watch源码分析』
watch
除了接受三个参数外,watch
会返回一个停止监听函数。并且还会在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。然后它会去调用doWatch
这个函数,调用doWatch
函数来实现相关逻辑。
doWatch
是watch
的核心,doWatch
函数为了兼容各个api
的逻辑源码,源代码比较长,若想阅读完整源码请戳这里。
接下来一起看看doWatch
函数。
『doWatch源码』
先看一下doWatch
的签名。
javascript
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }
: WatchOptions = EMPTY_OBJ): WatchStopHandle
『参数说明』
doWatch
也会接收『source: 监听的数据源、cb: 回调函数、options: 监听配置』
这三个参数,同样也有会返回一个停止监听函数。
『doWatch源码分析』
接着往下看。
scss
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
}
首先是在回调函数cb为null
的情况下,对immediate
和deep
进行校验,immediate
和deep
不为undefined
,就会警告immediate
和deep
只对有回调函数cb
的doWatch
签名有效。
其次声明了一个source参数不合法的警告函数。
typescript
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
紧接着声明了一些变量:
ini
const instance =
getCurrentScope() === currentInstance?.scope ? currentInstance : null
// const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
『说明』:
- instance:当前组件的实例,默认值
currentInstance
,currentInstance
是当前调用组件暴露出来的一个变量,方便该侦听器找到自己对应的组件。 - getter:会当做副作用的函数参数传入,在初始化effect时使用。
- forceTrigger:标识是否需要强制更新。
- isMultiSource:标记传入的是单个数据源还是以数组形式传入的多个数据源。
然后判断source
的类型,根据不同的类型重置『getter、forceTrigger、isMultiSource』
的值。
那就具体看一下是如何处理不同类型source
。
『source的类型是ref』:
ini
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
}
- getter 是个返回 source.value 的函数,访问 getter 函数会获取到 source.value 值,直接解包。
- forceTrigger 标记会根据 source 是否是 shallowRef 来设置。
『source的类型是reactive』:
ini
if (isReactive(source)) {
getter = () => source
deep = true
}
- getter 是个返回 source 的函数,访问 getter 函数直接返回 source,因为 reactive 的值不需要解包获取。
- 由于 reactive 中往往有多个属性,所以会将 deep 设置为 true,这里可以看出从外部给 reactive 设置 deep 是无效的。
『source的类型是数组』:
scss
if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
}
- 将 isMultiSource 设置为 true。
- forceTrigger 会根据数组中是否存在 reactive 类型的数据来判断。
- getter 是一个数组形式,会遍历source,针对不同类型的source,做相应的处理, 就会得到各个元素的单个 getter 结果。
『source的类型是function』:
scss
if (isFunction(source)) {
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
-
如果有回调函数cb
- getter 函数中会执行 source , source 会通过 callWithErrorHandling 函数执行, callWithErrorHandling 函数中会处理 source 执行过程中出现的错误。
『callWithErrorHandling源码』
typescript
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
『参数说明』:
- fn:要进行错误处理的函数
- instance:当前组件的实例
- type:fn执行过程中出现的错误类型
- args:是fn函数的参数
-
如果没有回调函数cb,那么此时就是 watchEffect api 的场景了。
-
此时会为 watchEffect 设置 getter 函数,getter 函数逻辑如下:
- 如果组件实例已经卸载,则不执行,直接返回
- 否则判断 cleanup ,cleanup 是在 watchEffect 中通过 onCleanup 注册的清理函数,存在 cleanup ,执行 cleanup 清除依赖
- 执行 source 函数,并返回执行结果。 source 会 callWithAsyncErrorHandling 包装,callWithAsyncErrorHandling 会处理source 执行过程中出现的错误,会处理异步错误。
-
『callWithAsyncErrorHandling源码』
typescript
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
『参数说明』: callWithAsyncErrorHandling 与 callWithErrorHandling 不同的是,callWithAsyncErrorHandling 可以接受一个fn数组。
『source的类型不是以上情况』:
ini
{
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
- 如果 source 不是以上的情况,则将 getter 设置为空函数,并且报出 source 不合法的警告⚠️。
『Vue2的兼容处理』
doWatch
函数中,还做了一层Vue2的兼容处理,主要是通过对getter
函数进行了一层重载,并对getter
函数返回的value
进行了深度递归遍历。
scss
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
当有回调,并且 deep 选项为 true 时,将使用 traverse 来包裹 getter 函数,对数据源中的每个属性递归遍历进行监听。
『traverse源码』
scss
export function traverse(value: unknown, seen?: Set<unknown>) {
// 如果value不是对象或value不可被转为代理(经过markRaw处理),直接return value
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
// sean用于暂存访问过的属性,防止出现循环引用的问题
seen = seen || new Set()
// 如果seen中已经存在了value,意味着value中存在循环引用的情况,这时return value
if (seen.has(value)) {
return value
}
// 添加value到seen中
seen.add(value)
// Ref,递归访问value.value
if (isRef(value)) {
traverse(value.value, seen)
} else if (isArray(value)) {
// 如果是数组,遍历数组并调用traverse递归访问元素内的属性
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {
// 如果是Set或Map,调用traverse递归访问集合中的值如果是原始对象,调用traverse递归访问value中的属性
value.forEach((v: any) => {
traverse(v, seen)
})
} else if (isPlainObject(value)) {
// 如果是原始对象,调用traverse递归访问value中的属性
for (const key in value) {
traverse((value as any)[key], seen)
}
}
return value
}
traverse
函数的作用是对getter
的返回值做一个递归遍历,将遍历到的值添加到一个叫做seen
的集合中,seen
用于防止循环引用问题。seen
中的数据就是当前watch
要侦听的那些数据。
经过上面一系列的过程之后,『getter函数、forceTrigger、isMultiSource
』就已被确定下来了。
之后声明两个变量『cleanup
和onCleanup
』用于清除副作用,以及SSR
检测。
javascript
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__SSR__ && isInSSRComponentSetup) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return NOOP
}
onCleanup 会作为参数传递给 watchEffect 中的 effect 函数。当onCleanup 执行时,会将它的参数通过 callWithErrorHandling 封装赋给 cleanup 及 effect.onStop。
然后声明了一个oldValue和job变量。
typescript
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// job为当前watch要做的工作,后续通过调度器来处理
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
// cb存在,说明是watch,而不是watchEffect
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
// 兼容2.x
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 用异步异常处理程序包裹了一层来调用cb
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup
])
// cb执行完成,当前的新值就变成了旧值
oldValue = newValue
}
} else {
// cb不存在,则是watchEffect
// watchEffect
effect.run()
}
}
// 设置allowRecurse,让调度器知道它可以自己触发
job.allowRecurse = !!cb
『说明』:
- 如果是多数据源oldValue是个数组,否则是个对象。
- job函数的作用是触发回调函数cb(watch) 或执行 effect.run(watchEffect)。
job函数中会判断 effect 的激活状态,如果当前effect不在 active 状态,说明没有触发该 effect 的响应式变化,则直接返回。
然后判断如果存在回调函数cb,调用 effect.run 得到新的值 newValue。
下一步就是触发回调函数cb,下面是触发cb需要的条件:
- 深度监听deep===true
- 强制触发forceTrigger===true
- 取到的新值和旧值是否相同,如果有变化则进入分支。如果多数据源,newValue 中存在与 oldValue 中的值不相同的项(利用Object.is判断);如果不是多数据源,newValue 与 oldValue 不相同。
- 开启了 vue2 兼容模式,并且 newValue 是个数组,并且开启 WATCH_ARRAY
只要满足以上任意一个就可以触发回调函数cb,在触发回调函数cb之前会先调用 cleanup 函数。执行完回调函数cb后,需要将 newValue 赋值给 oldValue。
如果不存在回调函数cb,那么直接调用effect.run即可。
在watch
的分支出现了effect
,但是这个分支并没有effect
,原来是由之前取得的getter
来创建的effect
。
接下来还定义了调度器scheduler
,在 scheduler 中会根据 flush 的不同决定 job 的触发时机。(由于调度器scheduler
被混合在effect
里,就会影响newValue
的获取,所以也就会影响cb
的调用时机)
『说明』:
sync
:同步执行,直接将 job 赋值给 scheduler,这样这个调度器函数就会直接执行。pre
:默认值是pre
,表示组件更新前执行,调度器会区分组件是否已经挂载,副作用第一次调用时必须是在组件挂载之前,而挂载后则会被推入一个优先执行时机的队列中。post
:组件更新后执行,需要延迟执行时,将 job 传入 queuePostRenderEffect 中,这样 job 会被添加进一个延迟执行的队列中,这个队列会在组件被挂载后、更新的生命周期中执行。
typescript
let scheduler: EffectScheduler
// 根据flush的值来创建不同的调度器
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => queuePreFlushCb(job)
}
现在 getter 和 scheduler 完成。为 watch 创建 effect 。这里还有调试用的 onTrack 和 onTrigger,当收集依赖和触发更新时做一些操作。
ini
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
effect 实例创建完,就是侦听器的初始化调用副作用。第一次开始执行副作用函数,会有不同的情况。
『回调函数cb存在』
- 如果
immediate
为true。将直接调用一次job
,触发回调函数cb
。job
是包裹了一层错误处理程序来调用回调函cb
,所以immediate
能让回调函数cb
立即触发一次。 - 否则执行effect.run()进行依赖的收集,并将返回值赋值给 oldValue。
『flush === 'post'』
- 没有cb,是watchEffect,副作用的时机在组件更新之后,用queuePostRenderEffect包裹一层,将effect.run()推入一个延迟队列中,来调整时机。
『其他情况』
- 是 watchEffect,副作用的时机在组件更新之前,直接执行一次effect.run()。
scss
// initial run
// 有cb,是 watch
if (cb) {
if (immediate) {
job()
} else {
// 获取一下当前的值作为旧值
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
最后的最后返回一个函数,这个函数的作用是停止watch对数据源的监听。
在函数内部执行 effect.stop 来达到停止监听的作用,如果存在组件实例,并且组件示例中存在effectScope,那么移除当前实例作用域下的当前effect。
scss
return () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
以上就是watch
的源码了。