Vue3 computed & watch源码分析

大家好,我是剑大瑞。

这边文章主要分析Vue3中watch API的使用 & 实现原理,希望文中的内容对你有所帮助。

如果有错误之处还望指正。我会非常感谢你的。

如果没有对Vue3的响应式原理不熟悉,建议先阅读第一篇Vue3 reactivity 源码解析

Vue3 computed & watch源码分析_第1张图片

computed API

在上篇文章中我们分析了computed的原理。在runtime core中,有对computed做了一层处理,主要是记录当前实例的computed Effectinstance.effects。方便组件卸载的时候,清空依赖。

runtime core中的代码较为简单,我们一起简单看下。

  • 记录组件创建阶段的 computed Effect instance.effects,方便组件卸载的时候,移除当前实例的 computed effect
  • 我们使用的computed API就是经过runtime core处理过的computed
import {
  computed as _computed,
} from '@vue/reactivity'

// 来自component.ts文件
function recordInstanceBoundEffect(effect,instance = currentInstance) {
  if (instance) {
    ;(instance.effects || (instance.effects = [])).push(effect)
  }
}

function computed(getterOrOptions) {
  const c = _computed(getterOrOptions)
  // c.effect
  recordInstanceBoundEffect(c.effect)
  return c
}

watch API

Vue3中新增了一个响应式函数:watchEffect。在分析watch & watchEffect之前,我们一起先回顾下这两个API如何使用。

回顾

watchwatchEffect都属于Vue中的响应式API。

注意:一提到响应式,大家就应该想到:getter & tracksetter & trigger

watchEffect

  • watchEffect可以根据响应数据状态的变化,自动或者重新执行传入的副作用函数。
  • 他接受一个回调函数,并在创建的时候立即执行,同时对齐进行响应式依赖追踪
  • 即建立当前传入的回调函数与所有相关effect的依赖关系。
  • 并在依赖变化的时候重新运行该回调函数。
  • 并会返回一个stop函数,用来停止侦听,即断开当前watchEffect与其所有依赖的effect之间的关系
const count = ref(0)

const stop = watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

// 停止侦听
stop()
count.value += 1

当然watchEffect也可以接受异步回调函数作为参数。当回调函数为异步时:

  • watchEffect可以给传入的函数传递一个异步的onInvalidate函数作为入参,用来注册清理watchEffect失效时的回调函数
  • 何时watchEffect会失效:

    • 当手动调用stop函数的时候
    • 当组件卸载的时候

const stop = watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // 当调用stop函数时,会执行给onInvalidate传入的回调函数
    token.cancel()
  })
})
onUnmounted(() => {
    console.log('组件卸载')
})

为了提高刷新效率,Vue的响应式系统会缓存并异步处理所有watchEffect副作用函数,以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。

关于如何缓存并异步处理,稍后源码中进行解析

配置watchEffectwatchEffect可以接受两个参数,第二个参数对watchEffect进行配置:

  • 默认情况下(flush: 'pre')watchEffect副作用会在所有的组件 update 执行
  • 当设置flush: 'post'时,组件更新后会重新运行watchEffect副作用
  • 当设置flush: 'sync'时,这将强制效果始终同步触发watchEffect副作用


watch

  • watch等同于组件侦听器property
  • 需要侦听特定的响应式数据源
  • 并在回调喊胡世宗执行副作用
  • 默认情况下是惰性的,只有当侦听的数据源发生变化的时候才会执行回调
侦听单个数据源:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})
侦听多个数据源(直接侦听ref):
注意虽然侦听的是多个数据源,但是当多个数据源发生改变的时候,侦听器仍只会执行一次
setup() {
  const firstName = ref('')
  const lastName = ref('')

  watch([firstName, lastName], (newValues, prevValues) => {
    console.log(newValues, prevValues)
  })

  const changeValues = () => {
    firstName.value = 'John'
    lastName.value = 'Smith'
    // 打印 ["John", "Smith"] ["", ""]
  }

  return { changeValues }
}
侦听响应式对象
  • deep可进行深度侦听
  • immediate可进行立即侦听
const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
  () => state,
  (state, prevState) => {
    console.log('not deep', state.attributes.name, prevState.attributes.name)
  }
)
// 深度并立即响应侦听
watch(
  () => state,
  (state, prevState) => {
    console.log('deep', state.attributes.name, prevState.attributes.name)
  },
  { deep: true, immediate: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

这里需要说下 【副作用】、【依赖】都是我们上一篇文章中提到的effet

比较关键的是,我们这里接触的是Vue源码中的第二个级别的effect,第一个是compute Effect。这次要说的是watch Effect

Ok,到这里我们基本已经回顾完这两个响应式API如何使用了,下面我们结合源码,进行分析。

分析

通过回顾部分的内容,基本已经对watch & watchEffect有了点感觉,既然要分析源码实现,最好是带着问题来看:

  1. watchEffect是如何停止侦听的?
  2. watchEffect是如何进行函数缓存的?
  3. watchEffect是如何异步进行刷新的?
  4. watch是如何侦听单个或者多个数据源的?
  5. watch是如何进行深度或者立即侦听响应的?

Vue3中的watch代码中设计的功能比较多,为了方便理解,我们拆开来一点一点进行解析

watchEffect是如何停止侦听的?

前面提到watch其实也是一个effect,所谓的侦听就是watch与其他effect之间建立一个依赖关系,当数据发生变化的时候,去遍历执行所有的effect,就会执行watch

在上一篇文章中我们提到,effect中有个stop函数,用于断开传入effect与之相关的依赖之间的关系。

所谓的停止侦听就是断开watch与所有相关effect的依赖关系。

当创建watch Effect时,会为其维护一个deps属性,用于存储所有的dep。故当我们创建watch的时候,将当前runner传给stop函数,并返回一个函数,用户调用的时候,就会停止侦听。

下面代码我们暂时省略与停止侦听无关的代码,只需理解能解答问题的部分即可。

// reactive effect.ts 文件
export function stop(effect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}
function cleanup(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

// 真正的watch函数
function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance 
) {
  // 定义runner
  // watch 级别的effect
  // runner执行,即执行getter函数
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })

  
  // 返回一个stop函数
  // 用于断开runner与其他依赖之间的关系
  // 并将其将从instance.effects中移除
  return () => {
    stop(runner)
    // 
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

runner就是effect API创建的watch Effectwatch对数据源进行侦听的时候,就会与其他依赖之间建立关系。即effectdep之间相互耦合的关系。

当需要停止侦听的时候,通过调用doWatch返回的函数就可以断开runner与其他dep的依赖关系。

watch是如何侦听单个或者多个数据源的?

在回顾部分我们知道,watch可以进行多种数据响应式数据类型的监听。

watch侦听的数据源发生变化的时候就会执行callback。这就是前面我们说的响应式。

在使用watch时,doWatch会创建一个 getter函数,用于确定数据源与callback之间的关系。

getter函数用于获取数据源的更新后的值。当getter函数执行的时候,就会触发依赖收集。

所以Vue3是在getter函数中对数据源进行判断侦听的。下面我们先看下源码的getter部分,在继续分析。

function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance = currentInstance
) {
  /**
  *  省略部分代码...
  */ 
      
  let getter
  let forceTrigger = false
  let isMultiSource = false
  
  /* Start: 开始定义getter函数 */
  if (isRef(source)) {
      
    // 第一种情况: 
    // 源是ref类型
    getter = () => source.value
    forceTrigger = !!source._shallow
  } else if (isReactive(source)) {
      
    // 第二种情况: 
    // 源是响应式对象,响应式对象自动进行深度侦听
    getter = () => source
    deep = true
  } else if (isArray(source)) {
      
    // 第三种情况: 
    // 侦听多个源
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    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)) {
      
    // 第四种情况:  
    // 数据源是函数
    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()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  } else {
    // 数据源可能有问题 进行校验 提示
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }
  /* End: 定义getter函数结束 */

  /**
  *  省略部分代码...
  */ 
}

从上面的代码我们可以看出,doWatch内部共对数据源进行了四种情况的判断。

  • 并且当数据源是响应式数据类型时,会自动进行深度侦听
  • 当侦听的是多个数据源的时候,会进行递归遍历
  • 当数据源是函数的时候,getter函数最终会调用 callWithAsyncErrorHandling异步执行侦听的函数。
  • deep为ttrue时,会对getter进行递归遍历。

再结合上面分析停止侦听的代码,可以知道,当runner函数执行的时候,就是执行getter获取数据源新值的时候。

深度侦听函数:

  • 是一个深度递归遍历函数
  • 通过seen函数判断循环引用的情况
  • 最终返回的是原始类型数据
//  递归遍历获取值,seen用于防止陷入死循环
function traverse(value, seen = new Set()) {
  if (
    !isObject(value) ||
    seen.has(value) ||
    (value)[ReactiveFlags.SKIP]
  ) {
    return value
  }
    
  seen.add(value)
    
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value)[key], seen)
    }
  }
  return value
}

watchEffect是如何进行函数缓存 & 异步进行刷新的?

我们知道当使用effect函数创建runner的时候,其实是创建了一个watch Effect

这里回顾下,在computed中,Vue3是直接将effect返回的函数,赋给ComputedRefImpl实例的effect属性,并在实例的getter函数中调用this.effect,从而获取 value。

在使用watch的时候,我们并没有直接或间接使用effect函数返回的watch Effect函数获取新值。也没有必要这么使用。

但是我们需要在相关依赖发生变化的时候重新执行watch Effect获取新值 & 执行callback。那该如何做?

答案是通过给watch Effect 配置scheduler属性。

当进行响应派发的时候,会触发trigger函数,trigger函数最终会遍历执行所有相关effect

在执行effect的过程中会判断effect.scheduler是否存在,如果存在就会执行scheduler函数。

watch Effect就是在scheduler中做的副作用函数的缓存和异步刷新的。

还是原来的套路,让我们先看下scheduler源码部分:

// 真正的watch函数
function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance = currentInstance
) {

  /**
  *  省略部分代码...
  */ 

  // 调度器,有没有想到computed API 创建的时候,在配置项中设置的 scheduler
  // 在computed中scheduler主要负责重置 dirty
  // 当 watche Effect 侦测的数据源发生变化的时候
  // 会进行trigger,遍历执行所有与数据源相关的 effect
  // 在遍历的过程中会判断effect.scheduler 是否存在
  // 如果存在 则会执行scheduler(任务调度器),这一点与我们第一篇提到的computed的原理一样
  // scheduler执行 其实就是在执行job,job执行就是在执行 runner Effect
  // 即 watch Effect
  let scheduler
  if (flush === 'sync') {
    // 同步更新
    scheduler = job // 任务调度函数被直接调用
  } else if (flush === 'post') {
    // 组件更新后
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // 默认情况下
    // 通过queuePreFlushCb缓存job,并异步更新副作用函数
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // 使用 'pre' 选项,第一次调用必须在组件安装之前发生,以便同步调用。
        job()
      }
    }
  }
  // 创建watch Effect
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })
  /**
  *  省略部分代码...
  */ 
}

通过上面的代码,我们简单分析scheduler的创建过程:

  • 主要与创建watch时配置的flush有关
  • 在默认下情况下scheduler内部通过queuePreFlushCb将job缓存在待执行队列中,并通过Promise.resolve异步更新队列从而避免不必要的重复调用
  • 通过Promise创建微任务。在update之前执行所有的副作用函数,等于是提高了副作用函数的优先级

这里我们先知道watchEffect是通过queuePreFlushCb做到的副作用函数缓存 & 异步批量更新。在后续的文章中会分析scheduler.ts部分的内容。到时候就会明白其作用。

另:基础薄弱的同学,建议熟悉下浏览器的宏任务与微任务相关知识。

在上面的代码中,可以知道scheduler主要的职责就是根据情况对job进行处理,那job是什么?

job 就是异步队列中的一个个任务。主要负责:

  • 通过判断callback,对watchwatchEffect进行判断
  • 通过执行runner获取新值
  • 通过callWithAsyncErrorHandling对callback函数进行异步处理,并将新旧值传给callback,这也是我们为什么可以在watch中拿到侦听数据源,变化前后value的原因。

下面一起看下job部分的代码实现:

// Simple effect.
// watchEffect 的创建方式
function watchEffect(
  effect,
  options
) {
  return doWatch(effect, null, options)
}

// watch 的创建方式
function watch (source, cb, options) {
  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, cb, options)
}
// 真正的watch函数
function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance = currentInstance
) {

  /**
  *  省略部分代码...
  */ 
      
  let cleanup 

  // 定义失效时需要传参的函数
  let onInvalidate = (fn) => {
    // 用于执行用户传进来的fn函数
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE

  // 定义任务队列中的任务
  // 用于执行runner函数
  // 执行的过程会进行track & trigger
  const job = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      // runner执行就是在执行getter函数,获取newValue
      const newValue = runner()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue).some((v, i) =>
              hasChanged(v, (oldValue)[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
          
        if (cleanup) {
          cleanup()
        }
          
        // 执行回调函数
        // 因为我们在传入的cb中很有可能读取或者更改响应式数据
        // 因此可能会进行 track || trigger
        // 将newValue & oldValue传给cb
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue, 
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        // 将新值赋值给旧值
        oldValue = newValue
      }
    } else {
        
      // watchEffect(effect)
      // watchEffect
      // watchEffect API的处理方式,直接执行runner
      runner()
    }
  }

  // 将job标记为一个可以侦测的回调函数,以便调度器知道他可以自动进行响应触发(trigger)
  job.allowRecurse = !!cb

  /**
  *  省略部分代码...
  */ 
}

通过上面代码,可以知道:

  • 对于watchEffect,执行job,就是在直接执行runner函数
  • 对于watch,首先需要通过runner获取新的value,并将新旧值传给callback函数。

watch是如何进行深度或者立即侦听响应的?

这里就很简单了,直接上代码:

深度侦听就是去遍历递归原来的getter函数

立即侦听即直接执行job函数,触发runner,并执行callback

// 真正的watch函数
function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance = currentInstance
) {
  /**
  *  省略部分代码...
  */ 
  // 深度侦听
  if (cb && deep) {
    // 深度侦听,则递归遍历getter函数返回的值
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

 /**
  *  省略部分代码...
  */ 

  // initial run
  if (cb) {
     // 立即响应侦听
    if (immediate) {
      // 立即执行
      // 即进行track & trigger
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }
 
 /**
  *  省略部分代码...
  */ 
}

如何做Vue2的兼容处理

Vue3在doWatch函数中,还做了一层Vue2的兼容处理,主要是通过对getter函数进行了一层重载,并对getter函数返回的value进行了深度递归遍历。

// 真正的watch函数
function doWatch(
  source,
  cb,
  { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ,
  instance = currentInstance
) {
  
  /**
  *  省略部分代码...
  */ 
 
  // 2.x array mutation watch compat
  // Vue2做兼容处理
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
  
 /**
  *  省略部分代码...
  */ 
}

总结

  • 通过上面的分析,我们现在掌握了两个源码级别的Effect,一个是computed Effect,一个是watch Effect
  • watch能对数据源进行响应式侦听。主要是通过将数据源转化为getter函数。
  • 并通过effect建立watch Effect与相关依赖之间的关系。
  • 当数据源发生变化的时候,会触发Trigger,进行响应派发,遍历执行所有相关的effect
  • effect.scheduler存在时,就会执行scheduler函数,而watch内部通过scheduler,对job任务进行了缓存,并放在一个待执行队列中,在update前,会通过promise异步执行job任务。
  • job执行,就会获取数据源变化后的值,并将新旧value传给用户创建watch时的回调函数。完成侦听任务。
不要忘记,在job中获取新值也会触发Track任务。

Vue3 computed & watch源码分析_第2张图片

最后让我们看下完整的watch相关部分的代码:

//  watcheffect.
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}
//  进行重载,侦听多个数据源 & cb
export function watch<
  T extends MultiWatchSources,
  Immediate extends Readonly = false
>(
  sources: [...T],
  cb: WatchCallback, MapSources>,
  options?: WatchOptions
): WatchStopHandle

//  重载:侦听多个数据源,并且数据源是只读的
export function watch<
  T extends Readonly,
  Immediate extends Readonly = false
>(
  source: T,
  cb: WatchCallback, MapSources>,
  options?: WatchOptions
): WatchStopHandle

//  重载:简单watch Effect & cb
export function watch = false>(
  source: WatchSource,
  cb: WatchCallback,
  options?: WatchOptions
): WatchStopHandle

//  重载:侦听响应式对象 & cb
export function watch<
  T extends object,
  Immediate extends Readonly = false
>(
  source: T,
  cb: WatchCallback,
  options?: WatchOptions
): WatchStopHandle

//  执行创建 watch
export function watch = false>(
  source: T | WatchSource,
  cb: any,
  options?: WatchOptions
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
     // 省略...
  }
  // 返回的是一个stop函数
  return doWatch(source as any, cb, options)
}
// 真正的watch函数
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {
  //  dev环境下判断 immediate, deep
  if (__DEV__ && !cb) {
    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
  let isMultiSource = false
  
  /* Start: 开始定义getter函数 */
  if (isRef(source)) {
    // 源是ref类型
    getter = () => source.value
    forceTrigger = !!source._shallow
  } else if (isReactive(source)) {
      
    //  源是响应式对象,自动进行深度侦听
    getter = () => source
    deep = true
  } else if (isArray(source)) {
      
    //  侦听多个源,遍历递归侦听
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    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)) {
    //  数据源是函数
    if (cb) {
      // 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,
          [onInvalidate]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }
  /* End: 定义getter函数结束 */

  //  Vue2做兼容处理
  if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }

  if (cb && deep) {
    // 深度侦听,则递归遍历getter函数返回的值
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  let cleanup: () => void

  //  定义失效时需要传参的函数
  let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }

  //  服务端渲染的情况下,不必创建一个真正的effect, onInvalidate 应该为一个空对象,
  // 触发 immediate 为true
  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 = isMultiSource ? [] : INITIAL_WATCHER_VALUE

  //  定义任务队列中的任务,用于执行runner函数,执行的过程会进行track & trigger
  const job: SchedulerJob = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      // runner执行就是在执行getter函数,获取newValue
      const newValue = runner()
      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))
      ) {
        // watch API的处理方式
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        // 执行回调函数
        // 因为我们在传入的cb中很有可能读取或者更改响应式数据
        // 因此可能会进行 track || trigger
        // 将newValue & oldValue传给cb
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue, 
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        // 将新值赋值给旧值
        oldValue = newValue
      }
    } else {
      // watchEffect
      // watchEffect API的处理方式,直接执行runner
      runner()
    }
  }

  //  将job标记为一个可以侦测的回调函数,以便调度器知道他可以自动进行响应触发(trigger)
  job.allowRecurse = !!cb
      
  // 
  // 调度器,有没有想到computed API 创建的时候,在配置项中设置的 scheduler
  // 在computed中scheduler主要负责重置 dirty
  // 当 watche Effect 侦测的数据源发生变化的时候
  // 会进行trigger,遍历执行所有与数据源相关的 effect
  // 在遍历的过程中会判断effect.scheduler 是否存在
  // 如果存在 则会执行scheduler(任务调度器),这一点与我们第一篇提到的computed的原理一样
  // scheduler执行 其实就是在执行job,job执行就是在执行 runner Effect
  // 即watch Effect
  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') {
    // 同步更新
    scheduler = job as any // 任务调度函数被直接调用
  } else if (flush === 'post') {
    // 组件更新后
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    // 默认情况下
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // 使用 'pre' 选项,第一次调用必须在组件安装之前发生,以便同步调用。
        job()
      }
    }
  }

  //  定义runner, watch 级别的effect,runner执行,即执行getter函数
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })
  
  // 将watch effect 存至instance.effects,当组件卸载的时候会清空当前runner与依赖之间的关系
  recordInstanceBoundEffect(runner, instance)

  // initial run
  if (cb) {
    if (immediate) {
      // 立即执行,即进行track & trigger
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }

  // 返回一个stop函数,用于断开runner与其他依赖之间的关系,并将其将从instance.effects中移除
  return () => {
    stop(runner)
    // 
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

//  this.$watch,组件实例上的watch API
export function instanceWatch(
  this: ComponentInternalInstance,
  source: string | Function,
  value: WatchCallback | ObjectWatchOptionItem,
  options?: WatchOptions
): WatchStopHandle {
  const publicThis = this.proxy as any
  //  定义getter函数
  const getter = isString(source)
    ? source.includes('.')
      ? createPathGetter(publicThis, source)
      : () => publicThis[source]
    : source.bind(publicThis, publicThis)
  let cb
  if (isFunction(value)) {
    cb = value
  } else {
    cb = value.handler as Function
    options = value
  }
  return doWatch(getter, cb.bind(publicThis), options, this)
}

//  获取侦听路径
export function createPathGetter(ctx: any, path: string) {
  const segments = path.split('.')
  return () => {
    let cur = ctx
    for (let i = 0; i < segments.length && cur; i++) {
      cur = cur[segments[i]]
    }
    return cur
  }
}

//  递归遍历获取值,seen用于防止陷入死循环
function traverse(value: unknown, seen: Set = new Set()) {
  if (
    !isObject(value) ||
    seen.has(value) ||
    (value as any)[ReactiveFlags.SKIP]
  ) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

如果文章中有错误之处,还望大佬们批评指正。

如果喜欢我的文章,可以关注 + 点赞。

如果需要持续了解Vue3源码分析系列,可关注我公共号【coder狂想曲】。在这里我们一起精进!年年double!

感谢阅读。

参考:

你可能感兴趣的:(Vue3 computed & watch源码分析)