乐趣区

关于前端:Vue3-computed-watch源码分析

大家好,我是剑大瑞。

这边文章次要剖析 Vue3 中 watch API 的应用 & 实现原理,心愿文中的内容对你有所帮忙。

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

如果没有对 Vue3 的响应式原理不相熟,倡议先浏览第一篇 Vue3 reactivity 源码解析。

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')watchEffec t 副作用会在所有的组件 update 执行
  • 当设置 flush: 'post' 时,组件更新后会从新运行 watchEffect 副作用
  • 当设置 flush: 'sync' 时,这将强制成果始终同步触发 watchEffect 副作用
<template>
  <div>{{count}}</div>
</template>

<script>
export default {setup() {const count = ref(0)
    // 更新前触发
    watchEffect(() => {console.log(count.value)
    }, {flush: 'pre'})
    // 更新后触发
    watchEffect(() => {console.log(count.value)
    }, {flush: 'post'})
    // 同步触发
    watchEffect(() => {console.log(count.value)
    }, {flush: 'sync'})
    return {count}
  }
}
</script>

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 为 t true时,会对 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 工作。

最初让咱们看下残缺的 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<boolean> = false
>(sources: [...T],
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

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

// 👉👉👉 重载:简略 watch Effect & cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle

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

// 👉👉👉 执行创立 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)) {// 省略...}
  // 返回的是一个 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.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<unknown> = 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!

感激浏览。

参考:

  • Vue 官网
  • Vue-next
退出移动版