Vue3源码梳理:watch监听函数的核心实现

概述

  • watch 可以监听响应式数据的变化,从而触发指定的函数
  • watch 函数监听三个参数
    • 监听的响应式对象
    • 回调函数 cb
    • 配置对象 options
      • imediate: 初始化完成后会被立刻触发一次
      • deep: 深度监听

Debug Watch 源码

  • 测试示例: watch.html,测试vue源码的watch函数

    <script src='../../dist/vue.global.js'>script>
    
    <body>
      <div id='app'>div>
    body>
    
    <script>
      const { reactive, watch } = Vue
      const obj = reactive({
        name: '张三'
      })
      watch(obj, (val, oldVal) => {
        console.log('watch ...')
        console.log(val)
      })
      const timer = setTimeout(() => {
        clearTimeout(timer)
        obj.name = '李四'
      }, 2000)
    script>
    
  • 进入 watch 函数,在源码 packages/runtime-core/src/apiWatch.ts 中

    // implementation
    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.`
        )
      }
      return doWatch(source as any, cb, options)
    }
    
  • 从这里可以看出,它本质上是一个 doWatch 函数的触发

    function doWatch(
      source: WatchSource | WatchSource[] | WatchEffect | object,
      cb: WatchCallback | null,
      // 这个参数解析了options对象
      { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
    ): WatchStopHandle {
      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.`
        )
      }
      // 这里引用一下 currentInstance
      const instance = currentInstance
      let getter: () => any
      let forceTrigger = false
      let isMultiSource = false
      // 注意这里,暂不会执行
      if (isRef(source)) {
        getter = () => source.value // ref 数据访问需要使用 .value的方式
        forceTrigger = isShallow(source)
      } else if (isReactive(source)) {
        // 注意这里会执行,因为我们定义的就是 reactive 响应式数据
        getter = () => source // 这里响应式数据,getter 直接被赋值
        deep = true // 这里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)) {
        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,
              [onCleanup]
            )
          }
        }
      } else {
        getter = NOOP
        __DEV__ && warnInvalidSource(source)
      }
    
      // 2.x array mutation watch compat
      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) {
        const baseGetter = getter
        getter = () => traverse(baseGetter()) // 这里做一层包装,先不管
      }
    
      let cleanup: () => void
      let onCleanup: OnCleanup = (fn: () => void) => {
        cleanup = effect.onStop = () => {
          callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
        }
      }
    
      // in SSR there is no need to setup an actual effect, and it should be noop
      // unless it's eager
      if (__SSR__ && isInSSRComponentSetup) {
        // we will also not call the invalidate callback (+ runner is not set up)
        onCleanup = NOOP
        if (!cb) {
          getter()
        } else if (immediate) {
          callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
            getter(),
            isMultiSource ? [] : undefined,
            onCleanup
          ])
        }
        return NOOP
      }
      // 这里有一个三元运算,会走到 INITIAL_WATCHER_VALUE 这里
      let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
      // 核心的job函数
      const job: SchedulerJob = () => {
        if (!effect.active) {
          return
        }
        if (cb) {
          // watch(source, cb)
          const newValue = effect.run()
          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
              oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
              onCleanup
            ])
            oldValue = newValue
          }
        } else {
          // watchEffect
          effect.run()
        }
      }
    
      // important: mark the job as a watcher callback so that scheduler knows
      // it is allowed to self-trigger (#1727)
      job.allowRecurse = !!cb // 这里看下 issue #1727
      // 这里声明调度器
      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 {
        // 此时调度器变成一个回调函数,包装了 job 函数
        // default: 'pre'
        scheduler = () => queuePreFlushCb(job)
      }
      // 这里同样走了老套路,传入 getter 和 调度器
      const effect = new ReactiveEffect(getter, scheduler)
    
      if (__DEV__) {
        effect.onTrack = onTrack
        effect.onTrigger = onTrigger
      }
      // 这里开始最核心的最后一块逻辑
      // initial run
      if (cb) {
        // 这里读取配置
        if (immediate) {
          job() // 这里可以看出job的执行会代表watch函数的自动触发
        } else {
          // 这里会执行, 执行run函数中的 fn 函数
          oldValue = effect.run()
        }
      } else if (flush === 'post') {
        queuePostRenderEffect(
          effect.run.bind(effect),
          instance && instance.suspense
        )
      } else {
        effect.run()
      }
      // 最终会return这个函数
      return () => {
        effect.stop() // 停止effect监听
        if (instance && instance.scope) {
          remove(instance.scope.effects!, effect)
        }
      }
    }
    
  • 这个watch是比较复杂的,里面核心的几个点:

    • scheduler 调度器和我们的effect实例两者之间相互作用
    • 一旦effect触发了 scheduler ,就会调用 queuePreFlushCb
    • 同时,我们知道,一旦job函数执行,因为回调cb的传递,watch函数必定会被触发
  • 当watch函数执行完毕,2s之后,测试代码中会触发 setter 行为

    const timer = setTimeout(() => {
       clearTimeout(timer)
       obj.name = '李四'
     }, 2000)
    
  • setter行为的触发本质上是之前收集的依赖被触发,也就是之前分析过的

    function triggerEffect(
      effect: ReactiveEffect,
      debuggerEventExtraInfo?: DebuggerEventExtraInfo
    ) {
      if (effect !== activeEffect || effect.allowRecurse) {
        if (__DEV__ && effect.onTrigger) {
          effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
        }
        if (effect.scheduler) {
          effect.scheduler()
        } else {
          effect.run()
        }
      }
    }
    
    • triggerEffects() 后续一系列的行为,因为有调度器的存在,所有会执行
      • effect.scheduler()
  • 这个调度器就是watch中的

    • scheduler = () => queuePreFlushCb(job)
    • 好,我们看下进入 queuePreFlushCb 函数
  • 这个函数 在 scheduler.ts 中

    // 注意,这里的 cb 参数,就是我们的核心job函数
    export function queuePreFlushCb(cb: SchedulerJob) {
      queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
    }
    
  • 其本质还是 queueCb 函数,进入

    function queueCb(
      cb: SchedulerJobs,
      activeQueue: SchedulerJob[] | null,
      pendingQueue: SchedulerJob[],
      index: number
    ) {
      if (!isArray(cb)) {
        if (
          !activeQueue ||
          !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
        ) {
          // 这里会执行,
          pendingQueue.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
        pendingQueue.push(...cb)
      }
      // 这里进入
      queueFlush()
    }
    
  • 进入 queueFlush

    function queueFlush() {
      // 判断状态,这里会进入
      if (!isFlushing && !isFlushPending) {
        isFlushPending = true
        currentFlushPromise = resolvedPromise.then(flushJobs)
      }
    }
    
  • 注意这里的 resolvedPromise 的定义,它实际就是Promise的resolve 函数

    const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
    
  • 也就是说通过异步微任务来处理队列任务,这里通过then函数执行 flushJobs,这个是最后的异步执行函数,会等待所有同步任务执行完成后立即被触发, 进入它

    function flushJobs(seen?: CountMap) {
      isFlushPending = false
      isFlushing = true
      if (__DEV__) {
        seen = seen || new Map()
      }
      // 这里会执行
      flushPreFlushCbs(seen)
    
      // 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.sort((a, b) => getId(a) - getId(b))
    
      // 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 {
        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
        currentFlushPromise = null
        // some postFlushCb queued jobs!
        // keep flushing until it drains.
        if (
          queue.length ||
          pendingPreFlushCbs.length ||
          pendingPostFlushCbs.length
        ) {
          flushJobs(seen)
        }
      }
    }
    
  • 进入内部的 flushPreFlushCbs 函数

    // 这个函数通过任务队列的形式,触发job函数
    export function flushPreFlushCbs(
      seen?: CountMap,
      parentJob: SchedulerJob | null = null
    ) {
      // 检测是否有数据,这里是之前的job函数
      if (pendingPreFlushCbs.length) {
        currentPreFlushParentJob = parentJob
        // 这里 activePreFlushCbs 取代了 pendingPreFlushCbs
        activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 去重
        pendingPreFlushCbs.length = 0 // 这里清空自己,保证下次不会被触发
        if (__DEV__) {
          seen = seen || new Map()
        }
        for (
          preFlushIndex = 0;
          preFlushIndex < activePreFlushCbs.length;
          preFlushIndex++
        ) {
          if (
            __DEV__ &&
            checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
          ) {
            continue
          }
          // 这里会执行,也就是job函数会被触发
          activePreFlushCbs[preFlushIndex]()
        }
        activePreFlushCbs = null
        preFlushIndex = 0
        currentPreFlushParentJob = null
        // recursively flush until it drains
        flushPreFlushCbs(seen, parentJob)
      }
    }
    
  • 既然上述函数是为了处理队列中的job函数,我们再回到job函数中看看,位置:apiWatch.ts 中,doWatch 函数中的一段逻辑

    const job: SchedulerJob = () => {
        if (!effect.active) {
          return
        }
        // 这里会执行
        if (cb) {
          // watch(source, cb)
          const newValue = effect.run() // 运行run函数,产生了 newValue, 这里是我们的响应性对象
          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()
            }
            // 这里是一个通用的 try catch
            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,
              onCleanup
            ])
            // 更新挂载 oldValue 变量
            oldValue = newValue
          }
        } else {
          // watchEffect
          effect.run()
        }
      }
    
  • 我们看一下这个 通用的 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
    }
    
    // 其实这个函数的位置,在 callWithAsyncErrorHandling 之上,目前是拿过来看看
    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
    }
    
  • 也就是说,vue内部,通过一个 callWithAsyncErrorHandling 函数来统一处理 所有可能出现的代码,统一进行try catch的操作

  • 从本质上来说 watch函数触发,是基于job函数触发来执行 effect.run 函数的调用,也就是会执行 我们的 fn 函数,所以,只要job被触发,watch就会被触发,当run() 执行了,就会拿到新的值

关于 scheduler 调度器的探究

  • 对于watch和computed都用到了我们的调度器,scheduler
  • 调度器包含两块内容
    • 懒执行
    • 调度器本身

1 )懒执行

  • 在effect.ts中的 effect 函数内部,有懒执行的相关逻辑
    if (!options || !options.lazy) {
      _effect.run()
    }
    
    • 这里可以看出懒执行为false时,run函数也会立即被执行,在watch里的懒执行是true,所以watch不会立即执行run函数

2 ) 调度器

  • 调度器主要分成两部分

    • 控制执行逻辑
    • 控制执行顺序
  • 测试调度器,新建测试用例 scheduler.html

    <script src='../../dist/vue.global.js'>script>
    
    <body>
      <div id='app'>div>
    body>
    
    <script>
      const { reactive, effect } = Vue
      const obj = reactive({
        count: 1
      })
      effect(() => {
        console.log(obj.count)
      })
      obj.count = 2
      console.log('over')
    script>
    
    • 这里的执行打印顺序是:1, 2, over

2.1 ) 控制代码执行顺序

  • 我们想,修改这个打印执行顺序,想要的顺序是: 1, over, 2

    • 这个意思是:effect函数的执行是懒的,是后发的,是异步的
  • 这时候,我们就需要用到的是调度器,我们修改下测试示例程序

      const { reactive, effect } = Vue
      const obj = reactive({
        count: 1
      })
      effect(() => {
        console.log(obj.count)
      }, {
        scheduler() {
          setTimeout(() => {
            console.log(obj.count)
          })
        }
      })
      obj.count = 2
      console.log('over')
    
    • 这时候的输出顺序就是: 1, over, 2
  • 在 effect.ts 中的 triggerEffect 函数中,有以下的流程控制

    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
    
  • 存在调度器,则会执行调度器,否则才会执行 run 函数,因为示例程序修改后,传入了 调度器,所以此时run函数不再会执行,因为调度器中传入的是异步任务,所以在同步任务执行完毕后,开始执行调度器里的流程

  • 所以,scheduler可以起到影响代码执行顺序的功能

2.2 ) 控制代码执行规则

  • 修改下测试用例的程序
    const { reactive, effect } = Vue
    const obj = reactive({
      count: 1
    })
    effect(() => {
      console.log(obj.count)
    })
    obj.count = 2
    obj.count = 3
    
  • 上述代码中有两次setter行为,实际上是不需要的,在vue源码中 queuePreFlushCb 可以完成这个需求
    • 在测试示例中,它会依次输出:1, 2, 3
    • 我们修改vue源码,导出这个 queuePreFlushCb 函数,来让用例使用
      export { nextTick, queuePreFlushCb } from './scheduler'
      
    • 修改完成vue源码后,执行下 $ npm run dev
    • 如果我们继续修改测试用例
        const { reactive, effect, queuePreFlushCb } = Vue
        const obj = reactive({
          count: 1
        })
        effect(() => {
          console.log(obj.count)
        }, {
          scheduler() {
            queuePreFlushCb(() => console.log(obj.count))
          }
        })
        obj.count = 2
        obj.count = 3
      
    • 这时候,只会输出 1, 3 达成我们的目标了
    • 实际上 在watch中的调度器就是 queuePreFlushCb 这个函数
      scheduler = () => queuePreFlushCb(job)
      
  • 这个调度器最终会触发 resolvedPromise.then 的函数,这个就是把我们当前队列扔进微任务中来顺序执行
  • 最终obj.count是3,最终在微任务中输出的也是3,因为微任务是在同步代码执行完毕后,才会去执行

删减后的核心实现代码

1 )实现调度器

  • 新建 packages/runtime-core/src/scheduler.ts
    let isFlushPending = false // 状态表示
    
    const pendingPreFlushCbs: Function[] = [] // 回调队列
    const resolvedPromise = Promise.resolve() as Promise<any>
    let currentFlushPromise: Promise<void> | null = null
    
    export function queuePreFlushCb(cb: Function) {
      queueCb(cb, pendingPreFlushCbs)
    }
    
    function queueCb(cb: Function, pendingQueue: Function[]) {
      pendingQueue.push(cb)
      queueFlush()
    }
    
    // 依次执行队列中的函数
    function queueFlush() {
      if (isFlushPending) return // 真,什么都不做
      // 只有这个状态为假,才继续执行
      isFlushPending = true
      currentFlushPromise = resolvedPromise.then(flushJobs)
    }
    
    // 处理队列
    function flushJobs() {
      isFlushPending = false
      flushPreFlushCbs()
    }
    
    // 用于循环进行队列处理
    export function flushPreFlushCbs() {
      if (!pendingPreFlushCbs.length) return
      // 有队列内容时,使用新的标识,进行内容拷贝
      let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
      pendingPreFlushCbs.length = 0 // 清空这个旧的队列
      for (let i = 0; i < activePreFlushCbs.length; i++) {
        activePreFlushCbs[i]()
      }
    }
    

2 )实现watch函数

  • 首先补充下 effect.ts 中的effect()函数和ReactiveEffect类,添加一个option参数,用于支持懒执行

    import { extend } from '@vue/shared' // 这个api就是 Object.assign,目的是通用
    
    export interface ReactiveEffectOptions {
      lazy?: boolean,
      scheduler?: EffectScheduler
    }
    
    export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions) {
      const _effect = new ReactiveEffect(fn)
      // 如果存在,则与_effect合并,这就意味着 options包含调度器,_effect中也会有调度器
      if (options) {
        extend(_effect, options)
      }
      if (!options || !options.lazy) {
        _effect.run()
      }
    }
    // 这个函数主要添加一个stop的空函数
    export class ReactiveEffect<T = any> {
      computed?: ComputedRefImpl<T>
      constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {}
      run() {
        activeEffect = this
        return this.fn()
      }
      // 后期实现
      stop() {}
    }
    
  • 再扩展 reactive.ts 补充 一个 isReactive 判断是否为响应性对象的函数,需要补充如下:

    // 定义枚举
    export const enum ReactiveFlags {
      IS_REACTIVE = '__v_isReactive' // 定义一个是 reactive 对象的私有属性标识
    }
    
    // 补充这个createReactiveObject函数
    function createReactiveObject(
      target: object,
      baseHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<object, any>
    ) {
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      const proxy = new Proxy(target, baseHandlers)
      // 添加下面这行
      proxy[ReactiveFlags.IS_REACTIVE] = true // 将私有属性标识标记为 true
      proxyMap.set(target, proxy)
      return proxy
    }
    
    // 判断是否是 响应式对象
    export function isReactive(value): boolean {
      return !!(value && value[ReactiveFlags.IS_REACTIVE])
    }
    
  • 新建 packages/runtime-core/src/apiWatch.ts

    import { EMPTY_OBJ, hasChanged } from '@vue/shared'
    import { isReactive, ReactiveEffect } from '@vue/reactivity'
    import { queuePreFlushCb } from './scheduler'
    
    export interface WatchOptions<immediate = boolean> {
      immediate?: immediate,
      deep?: boolean
    }
    
    export function watch(source, cb: Function, options?: WatchOptions) {
      return doWatch(source, cb, options)
    }
    
    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
      let getter: () => any
      // 当前是响应性对象
      if (isReactive(source)) {
        getter = () => source
        deep = true
      } else {
        // 否则
        getter  = () => {}
      }
      // 处理deep场景
      if (cb && deep) {
        // 这种写法需要注意:这里实际上是内部进行依赖收集的地方
        const baseGetter = getter // 浅拷贝引用
        getter = () => baseGetter()
      }
    
      let oldValue = {}
      const job = () => {
        if (cb) {
          const newValue = effect.run()
          if (deep || hasChanged(newValue, oldValue)) {
            cb(newValue, oldValue)
            oldValue = newValue
          }
        }
      }
      let scheduler = () => queuePreFlushCb(job)
    
      const effect = new ReactiveEffect(getter, scheduler)
      
      if (cb) {
        if (immediate) {
          job()
        } else {
          oldValue = effect.run()
        }
      } else {
        effect.run()
      }
      return () => {
        effect.stop()
      }
    }
    
  • 导出各项需要的api, 比如watch, 重新 npm run dev 后,创建watch测试程序 watch.html

    <script src="../dist/vue.js">script>
    <body>
      <div id='app'>div>
    body>
    
    <script>
      const { reactive, watch } = Vue
      const obj = reactive({
        name: '张三'
      })
      watch(obj, (val, oldVal) => {
        console.log('watch ...')
        console.log(val)
      }, {
        immediate: true // 添加这个配置,会立即执行一次,但是下面2s后的异步回调没有被执行,这里watch没有监听到 reactive 的变化
      })
      const timer = setTimeout(() => {
        clearTimeout(timer)
        obj.name = '李四'
      }, 2000)
    script>
    
  • 此时,watch是无法监听到reactive变化的, Why?

    • 因为我们没有进行依赖的收集,只有在 2s 的 setTimeout 函数中进行 setter 行为
    • 所以没有依赖可以触发
  • 实际上,我们自己的 apiWatcher.ts 中 getter = () => baseGetter() 这里,和源码是不一样的,Vue源码是 getter = () => traverse(baseGetter())

  • Vue源码中的 traverse函数就是为了一直收集getter行为,也就是说在测试程序中没有手动触发过getter行为, 我们要在watch内部把整个source对象里的所有属性,主动触发一次getter行为来完成依赖收集

  • 修改上述的 apiWatch.ts 中的getter, 如下

    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
      // ...
      // 处理deep场景
      if (cb && deep) {
        // 这种写法需要注意:这里是依赖收集的地方
        const baseGetter = getter // 浅拷贝引用
        getter = () => traverse(baseGetter()) // 内部进行依赖收集触发getter行为
      }
      // ...
    }
    
    export function traverse(value: unknown) {
      if (!isObject(value)) {
        return value
      }
      // 循环属性
      for (const key in value as object) {
        traverse((value as object)[key]) // 访问当前属性,触发 getter 行为
      }
      return value
    }
    
  • 这样,整个watch函数就可以正常运行了

总结

  • 整个watch的实现逻辑还是依赖于 reactiveEffect 的功能(依赖收集和触发的过程)
  • 和之前reactive, ref 的依赖收集,触发的区别在于 watch 中的依赖收集操作是被动收集的
  • 还有就是watch中的scheduler调度器,用于控制执行顺序和规则,本身是一个函数,内部配合各个方便实现调度功能, 比如在watch中的 queuePreFlushCb 函数

你可能感兴趣的:(Vue,Weex,Web,javascript,vue.js,前端)