vue3源码之旅-watch

1,282 阅读3分钟

更多文章

前言

之前已经了解了refreactiveeffectcomputed等响应式相关的api,接下来一起了解一下watchwatchEffect

简化代码

vue3-watch源码位置

watch、watchEffect

watchwatchEffect源码较为简单,均通过doWatch实现的逻辑,看下代码:

// watchEffect
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  // 返回dowatch
  return doWatch(effect, null, options)
}
// 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)) {
    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.`
    )
  }
  // 返回dowatch
  return doWatch(source as any, cb, options)
}

doWatch

doWatch完成了watchwatchEffect的核心逻辑,接下来以抽出来的简易代码来分析整个过程

function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ
) {
  let getter

  if(isRef(source)) {
    getter = () => source.value
  } else if (isReactive(source)) {
    getter = () => source
  } else if (isFunction(source)) {
    if(cb) {
      // watch
      getter = () => callWithErrorHandling(source)
    } else {
      // 非watch
      getter = () => {
        // cleanup存在执行清除副作用函数
        cleanup && cleanup()
        // 注册onInvalidate
        return callWithAsyncErrorHandling(source, [onInvalidate])
      }
    }
  } else {
    getter = NOOP
    warn(
      `Invalid watch source: `,
      source,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`
    )
  }
}

初始化阶段,根据入参的不同类型对getter做了不同的处理,getter会作为ReactiveEffect(不记得ReactiveEffect的话,翻一下之前的effect文章)的第一个参数传进去,当调用effect.run时会执行getter获取新值

callWithErrorHandlingcallWithAsyncErrorHandling分别是在watch和非watch时调用,看一下其各自的实现

简化版:

function callWithErrorHandling(fn, args) {
  const res = args ? fn(...args) : fn()
  return res
}

function callWithAsyncErrorHandling(fn, args) {
  const res = callWithErrorHandling(fn, args)
  return res
}

源码:

// callWithErrorHandling
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
}
// callWithAsyncErrorHandling
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
}

其目的一是为了执行传入的fn函数,二是对错误进行处理

在非watchcallWithAsyncErrorHandling注册了一个onInvalidate,这个函数就是我们对副作用进行处理时调用的函数,看一下其实现:

let cleanup
let onInvalidate = (fn) => {
  // 指定cleanup
  // 侦听器被停止时清除副作用
  cleanup = effect.onStop = () => callWithErrorHandling(fn)
}

当我们调用onInvalidate函数时,给cleanup赋值callWithErrorHandling,并将onInvalidate中的参数作为入参传给callWithErrorHandling,在副作用每次之前前调用effect即可达到清除副作用的目的

此处给effect注册一个onStop,停止监听时触发effect.stop达到清除副作用的目的

这里刚好解释了官网说的清除副作用调用时机

继续源码的分析,接下来就是当监听的值发生变化时触发cb即可:

let oldValue = INITIAL_WATCHER_VALUE

const job = () => {
  if (!effect.active) {
    return
  }
  // 触发cb
  if(cb) {
    const newValue = effect.run()
    // 副作用即将重新执行时清除副作用
    if (cleanup) {
      cleanup()
    }
    callWithAsyncErrorHandling(cb, [
      newValue,
      oldValue,
      onInvalidate
    ])
    oldValue = newValue
  } else {
    effect.run()
  }
}
// 执行job
let scheduler = () => job()
// 传入scheduler
const effect = new ReactiveEffect(getter, scheduler)

这里将之前的getterscheduler传入ReactiveEffect,每次调用effect.run时会触发getter获取新值,当监听的值发生变化时会触发scheduler,从而触发job -> callWithAsyncErrorHandling -> cb(cb存在即watch,cb不存在时初始化阶段已经注册了onInvalidate),最终返回可以中介监听的函数:

return () => {
  // 停止监听,清除副作用
  effect.stop()
}

基本到这里就结束了,可以参考简易代码后再去看源码,方便理解

结语

感觉还是需要总结一下语言,但又或者对源码理解不够