vue3 watchAPI的简单理解1

465 阅读5分钟

最近在看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进行递归(一次都不会🐶),而且会直接返回函数的执行结果,省去了递归这一步骤, 也算是对我们的项目性能有了一点小优化吧。

这次就先分享到这了.下次继续更新