Vue3源码解析之 watch

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 5 篇,关注专栏

前言

Vue3 中响应式系统除了 reactiveref 这两个函数外,我们还需了解下 computedwatch 这两个函数,它们也是响应式系统的关键所在,本篇我们来看下 watch 是如何实现的。

案例

首先引入 reactiveeffectwatch 三个函数,之后声明 obj 响应式数据,接着执行 watch 函数,该函数第一个参为监听数据,第二个参为监听回调,最后两秒后又修改 objname 值。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
        const { reactive, effect, watch } = Vue
        // 1. reactive 构建响应性数据
        const obj = reactive({
          name: 'jc'
        })

        // 2. 执行了 watch 函数
        watch(obj, (value, oldValue) => {
          console.log('watch 触发了')
          console.log(value)
        })

        // 3. 两秒后触发 setter 行为
        setTimeout(() => {
          obj.name = 'cc'
        }, 2000)
    </script>
  </body>
</html>

watch 实现

watch 函数定义在 packages/runtime-core/src/apiWatch.ts 文件下:

ts 复制代码
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  // 省略
  return doWatch(source as any, cb, options)
}

可以看出 watch 函数实际执行的是 doWatch 方法:

ts 复制代码
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  // 省略

  const instance = currentInstance
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  if (isRef(source)) {
    // 是否 ref 类型
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // 是否 reactive 类型
    getter = () => source
    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)
  }

  // 省略

  if (cb && deep) {
    const baseGetter = getter // getter 为 () => source
    getter = () => traverse(baseGetter())
  }

  // 省略
  
  // 定义 oldValue  isMultiSource 是否有多个源 [value1, value2] 需要监听
  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

  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 {
    // default: 'pre'
    scheduler = () => queuePreFlushCb(job) // 调度器赋值  也是核心逻辑
  }

  const effect = new ReactiveEffect(getter, scheduler)

  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }

  // initial run
  if (cb) {
    if (immediate) {
      // 默认自动执行 watch 一次
      job() // job 触发意味着 watch 被立即执行一次
    } else {
      oldValue = effect.run() // 等于执行 fn 函数 即 () => traverse(baseGetter())  即 () => source 即 传入的监听数据
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    effect.run()
  }

  return () => {
    effect.stop() // 监听停止
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
}

根据传入 source 监听数据类型不同走不同逻辑,当前 sourcereactive 类型,所以 getter 直接赋值为 () => source。另外还可以看到类型为 reactive 时,默认开启深度监听 deep = true 。由于存在 cb 监听回调和 deep,所以baseGetter 等于 getter ,即 () => sourcegetter 赋值为 () => traverse(baseGetter())

之后又定义了 oldValue 值,默认为空对象,也是回调函数中的 oldValue接着定义了一个 job 函数,这是 watch 的核心逻辑 ,我们稍后再来分析。然后又创建了一个调度器 scheduler ,我们在 computed 文中也提到过,在依赖触发时,会执行该方法。此时 scheduler 被赋值为 () => queuePreFlushCb(job),将 job 函数传入到 queuePreFlushCb 方法中,该逻辑之后来分析。

接着又创建了一个 ReactiveEffect 实例,将赋值后的 getterscheduler 传入,ReactiveEffect 的作用之前文章也提到过,这里不再具体说明。

由于存在 cb 回调函数,根据判断配置中 immediate 存在时,就执行 job 方法,我们可以理解为 job 的触发 watch 被立即执行一次。否则执行 effect.run 即执行 fn 方法,当前 fngetter() => traverse(baseGetter()),就是执行 () => source,结果为传入的监听对象 source

最后 oldValue 赋值:

此时 watch 函数执行完毕,两秒后触发 objsetter 行为,依赖触发 trigger 执行,再看下当前 effects

之后再遍历执行每个 effect,此时存在 scheduler 调度器,执行 scheduler 方法。当前 scheduler 为 之前赋值的 () => queuePreFlushCb(job),我们再来看下 queuePreFlushCb 方法,该方法定义在 packages/runtime-core/src/scheduler.ts 文件中:

ts 复制代码
export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

实际执行的是 queueCb 方法:

ts 复制代码
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()
}

该方法定义了一个 pendingQueue 队列数组,插入传入的 cb 回调即传入的 job 函数,然后执行 queueFlush 方法:

ts 复制代码
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

可以看出 watchjob 执行都是一个 微任务当前同步任务执行完毕后,执行微任务 ,之后执行 flushJobs 方法:

ts 复制代码
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(seen) 方法:

ts 复制代码
export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob // job 函数
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 取代 pendingPreFlushCbs
    pendingPreFlushCbs.length = 0 // 置空 下次不会再触发
    if (__DEV__) {
      seen = seen || new Map()
    }
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      activePreFlushCbs[preFlushIndex]() // 当前 job 函数执行
    }
    activePreFlushCbs = null
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // recursively flush until it drains
    flushPreFlushCbs(seen, parentJob)
  }
}

当前 pendingPreFlushCbs 为传入的 job 方法,之后将去重后的 pendingPreFlushCbs 赋值给 activePreFlushCbs,遍历执行 activePreFlushCbs[preFlushIndex](),实际是执行每个 job 函数,我们再回过来看下 job 函数:

ts 复制代码
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()
    }
  }

run 方法执行实际执行 getter() => traverse(baseGetter()) ,此时 newValue :

这里还得再看下 traverse 方法:

ts 复制代码
export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    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
}

该方法由于值类型不同,会递归处理返回最终的值。接着执行 callWithAsyncErrorHandling 方法:

ts 复制代码
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
}

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
}

可以看出 callWithErrorHandling 这里执行了 cb 回调函数即 watch 传入的匿名函数,callWithAsyncErrorHandling 主要是对错误统一监听处理。最后将 newValue 赋值给 oldValuewatch 至此执行完毕。

总结

  1. watch 函数实际执行的是 doWatch 方法。
  2. 调度器 schedulerwatch 中很关键。
  3. schedulerReactiveEffect 两者之间存在互相作用的关系,一旦 effect 触发了 scheduler ,那么会导致 queuePreFlushCb(job) 执行,job 方法就被塞入微任务的队列中。
  4. 只要 job() 触发,那么就表示 watch 触发了一次。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
相关推荐
秦jh_5 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑21318 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy19 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss