vue3源码学习7-侦听器watch
vue2.x中有两种使用侦听器的方式:
通过watch选项初始化:
// 这种创建的侦听器,会随组件的销毁停止对数据的侦听
export default {
watch: {
a(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
通过$watch API创建:
// 这种创建方式会返回一个unwatch函数,可以执行该函数停止侦听
const unwatch = vm.$watch('a', function(newVal, oldVal){
console.log(newVal, oldVal)
})
vue3中提供了3种用法:
watch API可以侦听一个函数,但是该函数必须返回一个响应式对象,当该响应式对象更新后,执行对应的回调:
import { reactive, watch } from 'vue'
const state = reactive({count: 0})
watch(()=>state.count, (count, prevCount) => {
// 当state.count更新,触发此回调
})
watch API可以直接侦听一个响应式对象,当响应式对象更新后,执行对应的回调
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (count, prevCount) => {
// 当count.value 更新,触发此回调
})
watch API还可以侦听多个响应式对象,任意一个响应式对象更新后,执行回调
import { ref, watch } from 'vue'
const count = ref(0)
const count2 = ref(0)
watch([count, count2], ([count, count2], [prevCount, prevCount2]) => {
// 当count.value或count2.value更新,触发此回调
})
watch API的实现:
packages/runtime-core/src/apiWatch.ts
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 非生产环境会判断参数cb是不是一个函数,不是的话,提示应该使用watchEffect API
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)
}
调用的doWatch 方法实现:
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// source不合法的时候警告
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.`
)
}
// 当前组件实例
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// source标准化主要是根据souce类型,将其变成标准的getter函数
if (isRef(source)) {
// 如果是ref对象,创建一个访问source.value的getter函数
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 如果是reactive对象,创建一个访问source的getter函数,并设置deep为true
getter = () => source
deep = true
} else 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)
}
})
} else if (isFunction(source)) {
// 如果source是一个函数,
if (cb) {
// 如果cb也存在,getter就是一个简单的对source函数封装的函数
// 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,
[onCleanup]
)
}
}
} else {
getter = NOOP
// 否侧,开发环境提示source不合法
__DEV__ && warnInvalidSource(source)
}
// 注册无效函数
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// 旧值初始化
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
// 求得新值
const newValue = effect.run()
// 如果deep的情况或者新旧值发生变化,执行cb函数
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))
) {
// 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
//第一次更改时传递旧值为undefined
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
// 更新旧值,为了下一次的比对
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// scheduler的作用是根据某种调度方式执行某种函数,这里主要影响回调函数的执行方式
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: EffectScheduler
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'
job.pre = true
if (instance) job.id = instance.uid
//进入异步队列,组件更新前执行
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, scheduler)
// initial run
// 初次执行
if (cb) {
if (immediate) {
job()
} else {
// 求旧值
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// 没有cb的情况
effect.run()
}
// 返回 侦听器销毁函数,可以调用该函数停止侦听
return () => {
effect.stop()
if (instance && instance.scope) {
// 移除组件effects对effect的引用
remove(instance.scope.effects!, effect)
}
}
}
销毁函数实现:
// packages/reactivity/src/effect.ts
stop() {
// stopped while running itself - defer the cleanup
// 让effect失活,清理effect相关依赖,这样就可以停止对数据的侦听
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
前面说到创建watcher的时候,如果配置flush为pre(默认)或者post, 那么watcher的回调函数就会异步执行,分别通过queueJob和queuePostRenderEffect把回调函数推入异步队列中packages/runtime-core/src/scheduler.ts:
// 异步任务队列
const queue: SchedulerJob[] = []
// 队列任务执行完成后执行的回调函数队列
const pendingPostFlushCbs: SchedulerJob[] = []
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
// 不涉及suspense的情况下,queuePostRenderEffect 就是queuePostFlushCb
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)
) {
pendingPostFlushCbs.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
// 如果是数组,把它拍平成一维
pendingPostFlushCbs.push(...cb)
}
queueFlush()
}
queueJob和queuePostFlushCb执行完毕后,都会执行queueFlush函数,来看它的实现:
// 维护了isFlushing和isFlushPending控制异步任务的刷新逻辑
let isFlushing = false
let isFlushPending = false
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 通过Promise.resolve().then异步执行flushJobs
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
然后看异步队列执行flushJobs的实现:
const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
}
return diff
}
function flushJobs(seen?: CountMap) {
// 开始执行的时候将isFlushPending重置为false,isFlushing设置为true表示正在执行异步任务队列
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 对于异步队列queue,遍历之前先做一次从小到大的排序:
// 1.创建组件的过程是由父到子,所以创建组件副作用函数也是先父后子,父组件的副作用渲染函数的effect id小于子组件的,
// 每次更新组件也是通过queueJob把effect推入异常队列queue中,所以为了保证先更新父组件再更新子组件,进行正序排序。
// 2.如果一个组件在父组件更新过程中被卸载,他自身应该被跳过。所以还是为了保证先更新父组件再更新子组件,进行正序排序。
queue.sort(comparator)
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
// 遍历执行queue中任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
// 开发环境下检测是否有循环更新
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
// 执行完异步队列任务后,执行回调函数队列
flushPostFlushCbs(seen)
// 回调函数队列执行完成后,会重置isFlushing为false,因为一些回调函数执行过程中还会再次添加异步任务,所以需要继续判断queue
// 或pendingPostFlushCbs队列中是否还存在任务,有则递归执行flushJobs,把他们执行完
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
flushPostFlushCbs的实现:
export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
// 因为在遍历过程中,可能某些回调函数的执行会再次修改pendingPostFlushCbs,
// 拷贝副本,这样就不会受到pendingPostFlushCbs修改的影响。
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested flushPostFlushCbs call
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
}
activePostFlushCbs[postFlushIndex]()
}
activePostFlushCbs = null
postFlushIndex = 0
}
}
上面执行异步任务队列和回调函数队列时,都有检测是否有循环更新,比如下面的例子:
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
state.count++
console.log(count)
})
state.count++
执行时,控制台会报错:Maximum recursive updates exceeded ,是因为在侦听中改了依赖数据,进入死循环,造成浏览器假死,所以vue实现了checkRecursiveUpdates方法:
// flushJobs方法一开始就定义一个map对象seen, 在checkRecursiveUpdates时,会把任务添加到seen中,并记录引用计数count,
// 初始值为1,如果两个队列中添加了相同的任务,则引用计数count加1,如果count大于100,说明相同的任务加了100次,正常使用中
// 不会出现这种情况,所以抛出错误
function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
if (!seen.has(fn)) {
seen.set(fn, 1)
} else {
const count = seen.get(fn)!
if (count > RECURSION_LIMIT) {
const instance = fn.ownerInstance
const componentName = instance && getComponentName(instance.type)
warn(
`Maximum recursive updates exceeded${
componentName ? ` in component <${componentName}>` : ``
}. ` +
`This means you have a reactive effect that is mutating its own ` +
`dependencies and thus recursively triggering itself. Possible sources ` +
`include component template, render function, updated hook or ` +
`watcher source function.`
)
return true
} else {
seen.set(fn, count + 1)
}
}
}
还有一个watchEffect API,注册一个副作用函数,副作用函数可以访问到响应式对象,当响应式对象变化后立即执行该函数,例如:
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => console.log(count.value))
count.value++
// 输出0, 1
watchEffect和watch区别
1.侦听源不同,watchEffect侦听的是普通函数,只要函数内部访问了响应式对象就行,不需要返回响应式对象
2.没有回调函数,副作用函数内部响应式对象变化后,直接执行该副作用函数
3.立即执行
来看一下它的实现packages/runtime-core/src/apiWatch.ts:
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
// cb 不存在的情况下简化代码
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
const instance = currentInstance
let getter: () => any
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()
}
// 执行source函数,传入onCleanup作为参数
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
let scheduler: EffectScheduler
// 创建scheduler
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'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
// 创建副作用函数
const effect = new ReactiveEffect(getter, scheduler)
// 立即执行
effect.run()
// 返回销毁函数
return () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
}
上面传入的onCleanup作用是什么?看个示例:
import {ref, watchEffect } from 'vue'
const id = ref(0)
watchEffect(onInvalidate => {
// 执行异步操作
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// 如果 id 发生变化或者 watcher 停止了,则执行逻辑取消前面的异步操作
token.cancel()
})
})
所以当执行onCleanup时,就是注册了一个effect的onStop方法叫做cleanup,这个方法内部会执行fn,也就是注册的无效回调函数。