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,也就是注册的无效回调函数。

posted @ 2022-09-08 18:03  菜菜123521  阅读(478)  评论(0编辑  收藏  举报