最近在看vue3.0的源码,对于watchAPI做一下简单的记录。 在vue3.0中, vue的响应式变成了由refAPI和reactiveAPI来创建的, refAPI对于一些基础的数据类型进行响应式的绑定, reactiveAPI则是对一些复杂的数据类型进行响应式的绑定。对应的watchAPI也有了一些响应的变化, 接下来就一起来分析一下吧。 首先先上一段代码
import {reactive, watch} from 'vue'
const state = reactive({count: 0})
watch(state, () => {
// 当数据发生改变时触发
})
可以看到, watch的写法几乎没有太大的变化,那么接下来让我们一起看一下watch内部的实现吧
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 判断watch函数的第二个参数是否是个函数,
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)
}
可以看到尤大的代码中watch函数简单的判断了一下watch中的第二个参数是否为一个函数, 当当前环境为开发环境并且第二个参数不是一个函数时,代码中会跑出一段警告。如果第二个参数为函数时,调用了一个叫doWatch的函数, 让我们一起看一下doWatch的实现吧.
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
instance = currentInstance
): WatchStopHandle {
if (__DEV__ && !cb) {
// 判断当前如果回调函数不存在时 添加immediate和deep时抛出警告
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
// 判断当前传入的监听对象是否为响应式
if (isRef(source)) {
getter = () => (source as Ref).value
forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) {
getter = () => source
// 如果监听对象是一个响应式的对象 则deep设为true
deep = true
} else if (isArray(source)) {
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, [
instance && (instance.proxy as any)
])
} else {
// 如果当前传入的数组的每一项不是函数、响应式对象 则返回警告
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
// 如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,这种情况下,getter 就是一个简单的对 source 函数封装的函数。
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
instance && (instance.proxy as any)
])
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK, //watcher callback
[onInvalidate]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
// 注册无效回调函数
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// ssr判断
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
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 = isArray(source) ? [] : INITIAL_WATCHER_VALUE // 空对象
const job: SchedulerJob = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = runner()
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
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 : oldValue,
onInvalidate
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: ReactiveEffectOptions['scheduler']
/**
* 当 flush 为 sync 的时候,表示它是一个同步 watcher,即当数据变化时同步执行回调函数。
当 flush 为 pre 的时候,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行。
如果没设置 flush,那么回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行
*/
if (flush === 'sync') {
scheduler = job
} else if (flush === 'post') {
// 进入异步队列,组件更新hou执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
// 进入异步队列,组件更新前执行
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
// 如果组件还没挂载,则同步执行确保在组件挂载前
job()
}
}
}
这段代码很长,我们一步步来看:
首先是判断当第二个参数不时一个Function时, 且开发者在第三个参数中传入了deep(深度监听)和immediate(组件创建时调用)时抛出一段警告。
然后判断了传入第一个参数的类型, 以下分为四种情况:
1. 当前参数为一个简单的响应式数据对象时,会直接返回当前的value
2. 当前参数为一个复杂的响应式数据对象时, 会强制把deep设为true, 然后通过traverse函数递归访问每一个子属性
3. 当前参数为一个函数时(这个主要是对于watch使用的优化, 后续会提到使用场景), 会调用一个叫callWithErrorHandling的函数,该函数会返回当前参数的执行结果
4. 当前参数为一个数组时, 调用数组的map方法, 判断数组的每一项是否为 ref|| reactive || Function 并执行相应的方法
5. 当前参数不是以上集中类型时, 抛出一段警告
接下来判断当回调函数存在且deep为true时(注意此时的source为一个复杂的响应式对象), 递归访问当前对象的子属性。 后面的暂时就先不说了(主要我也不很理解 哈哈), 讲一下什么情况下source会为一个参数,大家先看一段代码
import {reactive, watch} from 'vue'
const state = reactive({
count: {
a: {
b :0
}
}
})
在上面这段代码中, 如果我们直接对state对象进行监听的话, watch函数会递归访问state对象的每一个子属性, 这样会对性能造成一定的影响, 这时候我们可以使用以下的写法,大家请看:
watch(() => state.count.a.b, (newVal) => {
console.log(newVal)
})
为什么要这么写呢, 大家可以看一下上面, 这种写法不会调用traverse进行递归(一次都不会🐶),而且会直接返回函数的执行结果,省去了递归这一步骤, 也算是对我们的项目性能有了一点小优化吧。
这次就先分享到这了.下次继续更新