共计 18791 个字符,预计需要花费 47 分钟才能阅读完成。
大家好,我是剑大瑞。
这边文章次要剖析 Vue3 中 watch API
的应用 & 实现原理,心愿文中的内容对你有所帮忙。
如果有谬误之处还望斧正。我会非常感谢你的。
如果没有对 Vue3 的响应式原理不相熟,倡议先浏览第一篇 Vue3 reactivity 源码解析。
computed
API
在上篇文章中咱们剖析了 computed
的原理。在 runtime core
中,有对 computed
做了一层解决,次要是记录以后实例的 computed Effect
至instance.effects
。不便组件卸载的时候,清空依赖。
runtime core
中的代码较为简单,咱们一起简略看下。
- 记录组件创立阶段的
computed Effect
至instance.effects
,不便组件卸载的时候,移除以后实例的computed effect
- 咱们应用的
computed
API 就是通过runtime core
解决过的computed
import {computed as _computed,} from '@vue/reactivity'
// 来自 component.ts 文件
function recordInstanceBoundEffect(effect,instance = currentInstance) {if (instance) {;(instance.effects || (instance.effects = [])).push(effect)
}
}
function computed(getterOrOptions) {const c = _computed(getterOrOptions)
// c.effect
recordInstanceBoundEffect(c.effect)
return c
}
watch
API
Vue3 中新增了一个响应式函数:watchEffect
。在剖析 watch & watchEffect
之前,咱们一起先回顾下这两个 API 如何应用。
回顾
watch
与 watchEffect
都属于 Vue 中的响应式 API。
留神:一提到响应式,大家就应该想到:
getter & track
、setter & trigger
。
watchEffect
watchEffect
能够依据响应数据状态的变动,主动或者从新执行传入的副作用函数。- 他承受一个回调函数,并在创立的时候立刻执行,同时对齐进行响应式依赖追踪。
- 即建设以后传入的回调函数与所有相干
effect
的依赖关系。 - 并在依赖变动的时候从新运行该回调函数。
- 并会返回一个
stop
函数,用来进行侦听,即断开以后watchEffect
与其所有依赖的effect
之间的关系
const count = ref(0)
const stop = watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
// 进行侦听
stop()
count.value += 1
当然 watchEffect
也能够承受异步回调函数作为参数。当回调函数为异步时:
watchEffect
能够给传入的函数传递一个异步的onInvalidate
函数作为入参,用来注册清理watchEffect
生效时的回调函数-
何时
watchEffect
会生效:- 当手动调用
stop
函数的时候 - 当组件卸载的时候
- 当手动调用
const stop = watchEffect(onInvalidate => {const token = performAsyncOperation(id.value)
onInvalidate(() => {
// 当调用 stop 函数时,会执行给 onInvalidate 传入的回调函数
token.cancel()})
})
onUnmounted(() => {console.log('组件卸载')
})
为了进步刷新效率,Vue 的响应式零碎会缓存并异步解决所有 watchEffect
副作用函数,以防止同一个“tick
”中多个状态扭转导致的不必要的反复调用。
对于如何缓存并异步解决,稍后源码中进行解析
配置 watchEffect
,watchEffect
能够承受两个参数,第二个参数对 watchEffect
进行配置:
- 默认状况下
(flush: 'pre')
,watchEffec
t 副作用会在所有的组件update
前执行 - 当设置
flush: 'post'
时,组件更新后会从新运行watchEffect
副作用 - 当设置
flush: 'sync'
时,这将强制成果始终同步触发watchEffect
副作用
<template>
<div>{{count}}</div>
</template>
<script>
export default {setup() {const count = ref(0)
// 更新前触发
watchEffect(() => {console.log(count.value)
}, {flush: 'pre'})
// 更新后触发
watchEffect(() => {console.log(count.value)
}, {flush: 'post'})
// 同步触发
watchEffect(() => {console.log(count.value)
}, {flush: 'sync'})
return {count}
}
}
</script>
watch
watch
等同于组件侦听器property
- 须要侦听特定的响应式数据源
- 并在回调喊胡世宗执行副作用
- 默认状况下是惰性的,只有当侦听的数据源发生变化的时候才会执行回调
侦听单个数据源:
// 侦听一个 getter
const state = reactive({count: 0})
watch(() => state.count,
(count, prevCount) => {/* ... */}
)
// 间接侦听 ref
const count = ref(0)
watch(count, (count, prevCount) => {/* ... */})
侦听多个数据源(间接侦听ref
):
留神尽管侦听的是多个数据源,然而当多个数据源产生扭转的时候,侦听器仍只会执行一次
setup() {const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// 打印 ["John", "Smith"] ["",""]
}
return {changeValues}
}
侦听响应式对象
deep
可进行深度侦听immediate
可进行立刻侦听
const state = reactive({
id: 1,
attributes: {name: '',}
})
watch(() => state,
(state, prevState) => {console.log('not deep', state.attributes.name, prevState.attributes.name)
}
)
// 深度并立刻响应侦听
watch(() => state,
(state, prevState) => {console.log('deep', state.attributes.name, prevState.attributes.name)
},
{deep: true, immediate: true}
)
state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
这里须要说下【副作用】、【依赖】都是咱们上一篇文章中提到的
effet
。比拟要害的是,咱们这里接触的是 Vue 源码中的第二个级别的
effect
,第一个是compute Effect
。这次要说的是watch Effect
。
Ok,到这里咱们根本曾经回顾完这两个响应式 API 如何应用了,上面咱们联合源码,进行剖析。
剖析
通过回顾局部的内容,根本曾经对 watch & watchEffect
有了点感觉,既然要剖析源码实现,最好是带着问题来看:
watchEffect
是如何进行侦听的?watchEffect
是如何进行函数缓存的?watchEffect
是如何异步进行刷新的?watch
是如何侦听单个或者多个数据源的?watch
是如何进行深度或者立刻侦听响应的?
Vue3 中的 watch
代码中设计的性能比拟多,为了不便了解,咱们拆开来一点一点进行解析
watchEffect
是如何进行侦听的?
后面提到 watch
其实也是一个 effect
,所谓的侦听就是 watch 与其余effect
之间建设一个依赖关系,当数据发生变化的时候,去遍历执行所有的effect
,就会执行watch
。
在上一篇文章中咱们提到,effect
中有个 stop
函数,用于断开传入 effect
与之相干的依赖之间的关系。
所谓的进行侦听就是断开 watch
与所有相干 effect
的依赖关系。
当创立 watch Effect
时,会为其保护一个 deps
属性,用于存储所有的 dep
。故当咱们创立watch
的时候,将以后 runner
传给 stop
函数,并返回一个函数,用户调用的时候,就会进行侦听。
上面代码咱们临时省略与进行侦听无关的代码,只需了解能解答问题的局部即可。
// reactive effect.ts 文件
export function stop(effect) {if (effect.active) {cleanup(effect)
if (effect.options.onStop) {effect.options.onStop()
}
effect.active = false
}
}
function cleanup(effect) {const { deps} = effect
if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)
}
deps.length = 0
}
}
// 真正的 watch 函数
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance
) {
// 定义 runner
// watch 级别的 effect
// runner 执行,即执行 getter 函数
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
// 返回一个 stop 函数
// 用于断开 runner 与其余依赖之间的关系
// 并将其将从 instance.effects 中移除
return () => {stop(runner)
//
if (instance) {remove(instance.effects!, runner)
}
}
}
runner
就是 effect API
创立的 watch Effect
。watch
对数据源进行侦听的时候,就会与其余依赖之间建设关系。即 effect
与dep
之间互相耦合的关系。
当须要进行侦听的时候,通过调用 doWatch
返回的函数就能够断开 runner
与其余 dep
的依赖关系。
watch
是如何侦听单个或者多个数据源的?
在回顾局部咱们晓得,watch
能够进行多种数据响应式数据类型的监听。
当 watch
侦听的数据源发生变化的时候就会执行callback
。这就是后面咱们说的响应式。
在应用 watch
时,doWatch
会创立一个 getter
函数,用于确定数据源与 callback
之间的关系。
getter
函数用于获取数据源的更新后的值。当 getter
函数执行的时候,就会触发依赖收集。
所以 Vue3 是在 getter
函数中对数据源进行判断侦听的。上面咱们先看下源码的 getter
局部,在持续剖析。
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance = currentInstance
) {
/**
* 省略局部代码...
*/
let getter
let forceTrigger = false
let isMultiSource = false
/* Start: 开始定义 getter 函数 */
if (isRef(source)) {
// 👉👉👉第一种状况:// 源是 ref 类型
getter = () => source.value
forceTrigger = !!source._shallow
} else if (isReactive(source)) {
// 👉👉👉第二种状况:// 源是响应式对象,响应式对象主动进行深度侦听
getter = () => source
deep = true
} else if (isArray(source)) {
// 👉👉👉第三种状况:// 侦听多个源
isMultiSource = true
forceTrigger = source.some(isReactive)
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)
}
})
} else 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,
[onInvalidate]
)
}
}
} else {
// 数据源可能有问题 进行校验 提醒
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
/* End: 定义 getter 函数完结 */
/**
* 省略局部代码...
*/
}
从下面的代码咱们能够看出,doWatch
外部共对数据源进行了四种状况的判断。
- 并且当数据源是响应式数据类型时,会主动进行深度侦听
- 当侦听的是多个数据源的时候,会进行递归遍历
- 当数据源是函数的时候,
getter
函数最终会调用callWithAsyncErrorHandling
异步执行侦听的函数。 - 当
deep
为 ttrue
时,会对getter
进行递归遍历。
再联合下面剖析进行侦听的代码,能够晓得,当 runner
函数执行的时候,就是执行 getter
获取数据源新值的时候。
深度侦听函数:
- 是一个深度递归遍历函数
- 通过 seen 函数判断循环援用的状况
- 最终返回的是原始类型数据
// 👉👉👉 递归遍历获取值,seen 用于避免陷入死循环
function traverse(value, seen = new Set()) {
if (!isObject(value) ||
seen.has(value) ||
(value)[ReactiveFlags.SKIP]
) {return value}
seen.add(value)
if (isRef(value)) {traverse(value.value, seen)
} else if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)
})
} else if (isPlainObject(value)) {for (const key in value) {traverse((value)[key], seen)
}
}
return value
}
watchEffect 是如何进行函数缓存 & 异步进行刷新的?
咱们晓得当应用 effect
函数创立 runner
的时候,其实是创立了一个watch Effect
。
这里回顾下,在
computed
中,Vue3 是间接将effect
返回的函数,赋给ComputedRefImpl
实例的effect
属性,并在实例的getter
函数中调用 this.effect,从而获取 value。
在应用 watch
的时候,咱们并没有间接或间接应用 effect
函数返回的 watch Effect 函数获取新值。也没有必要这么应用。
然而咱们须要在相干依赖发生变化的时候从新执行 watch Effect
获取新值 & 执行callback
。那该如何做?
答案是通过给 watch Effect
配置scheduler
属性。
当进行响应派发的时候,会触发 trigger
函数,trigger
函数最终会遍历执行所有相干effect
。
在执行 effect
的过程中会判断 effect.scheduler
是否存在,如果存在就会执行 scheduler
函数。
而 watch Effect
就是在 scheduler
中做的副作用函数的缓存和异步刷新的。
还是原来的套路,让咱们先看下 scheduler
源码局部:
// 真正的 watch 函数
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance = currentInstance
) {
/**
* 省略局部代码...
*/
// 调度器,有没有想到 computed API 创立的时候,在配置项中设置的 scheduler
// 在 computed 中 scheduler 次要负责重置 dirty
// 当 watche Effect 侦测的数据源发生变化的时候
// 会进行 trigger,遍历执行所有与数据源相干的 effect
// 在遍历的过程中会判断 effect.scheduler 是否存在
// 如果存在 则会执行 scheduler(任务调度器),这一点与咱们第一篇提到的 computed 的原理一样
// scheduler 执行 其实就是在执行 job,job 执行就是在执行 runner Effect
// 即 watch Effect
let scheduler
if (flush === 'sync') {
// 同步更新
scheduler = job // 任务调度函数被间接调用
} else if (flush === 'post') {
// 组件更新后
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// 🚩🚩🚩默认状况下
// 🚩🚩🚩通过 queuePreFlushCb 缓存 job,并异步更新副作用函数
scheduler = () => {if (!instance || instance.isMounted) {queuePreFlushCb(job)
} else {
// 应用 'pre' 选项,第一次调用必须在组件装置之前产生,以便同步调用。job()}
}
}
// 创立 watch Effect
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
/**
* 省略局部代码...
*/
}
通过下面的代码,咱们简略剖析 scheduler
的创立过程:
- 次要与创立
watch
时配置的 flush 无关 - 在默认下状况下
scheduler
外部通过queuePreFlushCb
将 job 缓存在待执行队列中,并通过Promise.resolve
异步更新队列从而防止不必要的反复调用 - 通过
Promise
创立微工作。在update
之前执行所有的副作用函数,等于是进步了副作用函数的优先级
这里咱们先晓得
watchEffect
是通过queuePreFlushCb
做到的副作用函数缓存 & 异步批量更新。在后续的文章中会剖析scheduler.ts
局部的内容。到时候就会明确其作用。另:基础薄弱的同学,倡议相熟下浏览器的宏工作与微工作相干常识。
在下面的代码中,能够晓得 scheduler
次要的职责就是依据状况对 job
进行解决,那 job
是什么?
job
就是异步队列中的一个个工作。次要负责:
- 通过判断
callback
,对watch
与watchEffect
进行判断 - 通过执行
runner
获取新值 - 通过
callWithAsyncErrorHandling
对 callback 函数进行异步解决,并将新旧值传给callback
,这也是咱们为什么能够在watch
中拿到侦听数据源,变动前后 value 的起因。
上面一起看下 job
局部的代码实现:
// Simple effect.
// watchEffect 的创立形式
function watchEffect(
effect,
options
) {return doWatch(effect, null, options)
}
// watch 的创立形式
function watch (source, cb, options) {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, cb, options)
}
// 真正的 watch 函数
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance = currentInstance
) {
/**
* 省略局部代码...
*/
let cleanup
// 定义生效时须要传参的函数
let onInvalidate = (fn) => {
// 用于执行用户传进来的 fn 函数
cleanup = runner.options.onStop = () => {callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// 定义工作队列中的工作
// 用于执行 runner 函数
// 执行的过程会进行 track & trigger
const job = () => {if (!runner.active) {return}
if (cb) {// 🚩🚩🚩watch(source, cb)
// runner 执行就是在执行 getter 函数,获取 newValue
const newValue = runner()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue).some((v, i) =>
hasChanged(v, (oldValue)[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {if (cleanup) {cleanup()
}
// 执行回调函数
// 因为咱们在传入的 cb 中很有可能读取或者更改响应式数据
// 因而可能会进行 track || trigger
// 将 newValue & oldValue 传给 cb
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
// 将新值赋值给旧值
oldValue = newValue
}
} else {// 🚩🚩🚩watchEffect(effect)
// watchEffect
// watchEffect API 的解决形式,间接执行 runner
runner()}
}
// 将 job 标记为一个能够侦测的回调函数,以便调度器晓得他能够主动进行响应触发(trigger)job.allowRecurse = !!cb
/**
* 省略局部代码...
*/
}
通过下面代码,能够晓得:
- 对于
watchEffect
,执行job
,就是在间接执行runner
函数 - 对于
watch
,首先须要通过runner
获取新的value
,并将新旧值传给callback
函数。
watch 是如何进行深度或者立刻侦听响应的?
这里就很简略了,间接上代码:
深度侦听就是去遍历递归原来的 getter
函数
立刻侦听即间接执行 job
函数,触发runner
,并执行callback
。
// 真正的 watch 函数
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance = currentInstance
) {
/**
* 省略局部代码...
*/
// 👉深度侦听
if (cb && deep) {
// 深度侦听,则递归遍历 getter 函数返回的值
const baseGetter = getter
getter = () => traverse(baseGetter())
}
/**
* 省略局部代码...
*/
// initial run
if (cb) {
// 👉立刻响应侦听
if (immediate) {
// 立刻执行
// 即进行 track & trigger
job()} else {oldValue = runner()
}
} else if (flush === 'post') {queuePostRenderEffect(runner, instance && instance.suspense)
} else {runner()
}
/**
* 省略局部代码...
*/
}
如何做 Vue2 的兼容解决
Vue3 在 doWatch
函数中,还做了一层 Vue2 的兼容解决,次要是通过对 getter
函数进行了一层重载,并对 getter
函数返回的 value
进行了深度递归遍历。
// 真正的 watch 函数
function doWatch(
source,
cb,
{immediate, deep, flush, onTrack, onTrigger} = EMPTY_OBJ,
instance = currentInstance
) {
/**
* 省略局部代码...
*/
// 2.x array mutation watch compat
// Vue2 做兼容解决
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {const val = baseGetter()
if (isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {traverse(val)
}
return val
}
}
/**
* 省略局部代码...
*/
}
总结
- 通过下面的剖析,咱们当初把握了两个源码级别的
Effect
,一个是computed Effect
,一个是watch Effect
。 watch
能对数据源进行响应式侦听。次要是通过将数据源转化为getter
函数。- 并通过
effect
建设watch Effect
与相干依赖之间的关系。 - 当数据源发生变化的时候,会触发
Trigger
,进行响应派发,遍历执行所有相干的effect
。 - 当
effect.scheduler
存在时,就会执行scheduler
函数,而watch
外部通过scheduler
,对job
工作进行了缓存,并放在一个待执行队列中,在update
前,会通过promise
异步执行job
工作。 job
执行,就会获取数据源变动后的值,并将新旧value
传给用户创立watch
时的回调函数。实现侦听工作。
不要遗记,在 job 中获取新值也会触发
Track
工作。
最初让咱们看下残缺的 watch
相干局部的代码:
// 👉👉👉 watcheffect.
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}
// 👉👉👉 进行重载,侦听多个数据源 & cb
export function watch<
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 👉👉👉 重载:侦听多个数据源,并且数据源是只读的
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 👉👉👉 重载:简略 watch Effect & cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 👉👉👉 重载:侦听响应式对象 & cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 👉👉👉 执行创立 watch
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {if (__DEV__ && !isFunction(cb)) {// 省略...}
// 返回的是一个 stop 函数
return doWatch(source as any, cb, options)
}
// 👉👉👉真正的 watch 函数
function doWatch(source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{immediate, deep, flush, onTrack, onTrigger}: WatchOptions = EMPTY_OBJ,
instance = currentInstance
): WatchStopHandle {
// 👉👉👉 dev 环境下判断 immediate, deep
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.`
)
}
}
// 👉👉👉 校验数据源
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.`
)
}
let getter: () => any
let forceTrigger = false
let isMultiSource = false
/* Start: 开始定义 getter 函数 */
if (isRef(source)) {
// 源是 ref 类型
getter = () => source.value
forceTrigger = !!source._shallow
} else if (isReactive(source)) {
// 👉👉👉 源是响应式对象,主动进行深度侦听
getter = () => source
deep = true
} else if (isArray(source)) {
// 👉👉👉 侦听多个源,遍历递归侦听
isMultiSource = true
forceTrigger = source.some(isReactive)
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)
}
})
} else if (isFunction(source)) {
// 👉👉👉 数据源是函数
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
// 没有传回调函数的状况,watchEffect
getter = () => {if (instance && instance.isUnmounted) {return}
if (cleanup) {cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
/* End: 定义 getter 函数完结 */
// 👉👉👉 Vue2 做兼容解决
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) {
// 深度侦听,则递归遍历 getter 函数返回的值
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
// 👉👉👉 定义生效时须要传参的函数
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {cleanup = runner.options.onStop = () => {callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// 👉👉👉 服务端渲染的状况下,不用创立一个真正的 effect,onInvalidate 应该为一个空对象,// 触发 immediate 为 true
if (__NODE_JS__ && isInSSRComponentSetup) {// we will also not call the invalidate callback (+ runner is not set up)
onInvalidate = NOOP
if (!cb) {getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [getter(),
undefined,
onInvalidate
])
}
return NOOP
}
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// 👉👉👉 定义工作队列中的工作,用于执行 runner 函数,执行的过程会进行 track & trigger
const job: SchedulerJob = () => {if (!runner.active) {return}
if (cb) {// watch(source, cb)
// runner 执行就是在执行 getter 函数,获取 newValue
const newValue = runner()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// watch API 的解决形式
// cleanup before running cb again
if (cleanup) {cleanup()
}
// 执行回调函数
// 因为咱们在传入的 cb 中很有可能读取或者更改响应式数据
// 因而可能会进行 track || trigger
// 将 newValue & oldValue 传给 cb
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
// 将新值赋值给旧值
oldValue = newValue
}
} else {
// watchEffect
// watchEffect API 的解决形式,间接执行 runner
runner()}
}
// 👉👉👉 将 job 标记为一个能够侦测的回调函数,以便调度器晓得他能够主动进行响应触发(trigger)job.allowRecurse = !!cb
// 👉👉👉
// 调度器,有没有想到 computed API 创立的时候,在配置项中设置的 scheduler
// 在 computed 中 scheduler 次要负责重置 dirty
// 当 watche Effect 侦测的数据源发生变化的时候
// 会进行 trigger,遍历执行所有与数据源相干的 effect
// 在遍历的过程中会判断 effect.scheduler 是否存在
// 如果存在 则会执行 scheduler(任务调度器),这一点与咱们第一篇提到的 computed 的原理一样
// scheduler 执行 其实就是在执行 job,job 执行就是在执行 runner Effect
// 即 watch Effect
let scheduler: ReactiveEffectOptions['scheduler']
if (flush === 'sync') {
// 同步更新
scheduler = job as any // 任务调度函数被间接调用
} else if (flush === 'post') {
// 组件更新后
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 默认状况下
scheduler = () => {if (!instance || instance.isMounted) {queuePreFlushCb(job)
} else {
// 应用 'pre' 选项,第一次调用必须在组件装置之前产生,以便同步调用。job()}
}
}
// 👉👉👉 定义 runner,watch 级别的 effect,runner 执行,即执行 getter 函数
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
// 👉👉👉将 watch effect 存至 instance.effects,当组件卸载的时候会清空以后 runner 与依赖之间的关系
recordInstanceBoundEffect(runner, instance)
// 👉👉👉initial run
if (cb) {if (immediate) {
// 👉👉👉立刻执行,即进行 track & trigger
job()} else {oldValue = runner()
}
} else if (flush === 'post') {queuePostRenderEffect(runner, instance && instance.suspense)
} else {runner()
}
// 👉👉👉返回一个 stop 函数,用于断开 runner 与其余依赖之间的关系,并将其将从 instance.effects 中移除
return () => {stop(runner)
//
if (instance) {remove(instance.effects!, runner)
}
}
}
// 👉👉👉 this.$watch,组件实例上的 watch API
export function instanceWatch(
this: ComponentInternalInstance,
source: string | Function,
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions
): WatchStopHandle {
const publicThis = this.proxy as any
// 👉👉👉 定义 getter 函数
const getter = isString(source)
? source.includes('.')
? createPathGetter(publicThis, source)
: () => publicThis
: source.bind(publicThis, publicThis)
let cb
if (isFunction(value)) {cb = value} else {
cb = value.handler as Function
options = value
}
return doWatch(getter, cb.bind(publicThis), options, this)
}
// 👉👉👉 获取侦听门路
export function createPathGetter(ctx: any, path: string) {const segments = path.split('.')
return () => {
let cur = ctx
for (let i = 0; i < segments.length && cur; i++) {cur = cur[segments[i]]
}
return cur
}
}
// 👉👉👉 递归遍历获取值,seen 用于避免陷入死循环
function traverse(value: unknown, seen: Set<unknown> = new Set()) {
if (!isObject(value) ||
seen.has(value) ||
(value as any)[ReactiveFlags.SKIP]
) {return value}
seen.add(value)
if (isRef(value)) {traverse(value.value, seen)
} else if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)
})
} else if (isPlainObject(value)) {for (const key in value) {traverse((value as any)[key], seen)
}
}
return value
}
如果文章中有谬误之处,还望大佬们批评指正。
如果喜爱我的文章,能够关注 + 点赞。
如果须要继续理解 Vue3 源码剖析系列,可关注我公共号【coder 狂想曲】。在这里咱们一起精进!年年 double!
感激浏览。
参考:
- Vue 官网
- Vue-next