Vue3源码分析(四)- watch 原理

163 阅读5分钟

区别

  • watch:监听特定的数据源,在第一次加载的时候不执行函数,只有当监听的数据源发生变化时才会运行
  • watchEffect:立即执行传入的函数,同时响应式追踪依赖,并在依赖更新时重新运行该函数,不需要传入一个数据源,只需传入一个函数,在函数中使用到的响应式数据都会追踪依赖,且在页面第一次加载的时候就会执行

watchEffect

// packages/runtime-core/src/apiWatch.ts

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase,
): WatchStopHandle {
  return doWatch(effect, null, options)
}

watch

// packages/runtime-core/src/apiWatch.ts

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) // 传入响应式数据

export function watch<T = any, Immediate extends Readonly<boolean> = false>( // Immediate默认值为 false,表示默认不立即执行
  source: T | WatchSource<T>, // 数据源
  cb: any, // 回调函数
  options?: WatchOptions<Immediate>, // 配置项:immediate 立即执行、deep 深度监听、once 监听一次

): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

doWatch

// packages/runtime-core/src/apiWatch.ts

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {
    immediate,
    deep,
    flush,
    once,
    onTrack,
    onTrigger,
  }: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
// once为 true 
    if (cb && once) {
      const _cb = cb
      cb = (...args) => {
        _cb(...args) // 调用一次
        unwatch() // 停止监听
      }
    }

    const instance = currentInstance // 方便监听器找到自己对应的组件
    let getter: () => any // 作为副作用函数参数传入
    let forceTrigger = false // 是否需要强制更新
    let isMultiSource = false // 标记传入的是单个数据源还是以数组形式传入的多个数据源

    // 递归遍历数组或对象
    const reactiveGetter = (source: object) =>
      deep === true
        ? source // traverse will happen in wrapped getter below
        : 
          traverse(source, deep === false ? 1 : undefined) // 仅监听根层级的属性
      
    if (isRef(source)) { 
    // 监听的数据源是一个 ref 类型的数据
      getter = () => source.value // 解包
      forceTrigger = isShallow(source) // 判断是否为浅响应式来完成 forceTrigger 变量的初始化
    } else if (isReactive(source)) {  
    // 监听的数据源是一个响应式数据
      getter = () => reactiveGetter(source)
      forceTrigger = 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 reactiveGetter(s)
          } else if (isFunction(s)) {
            return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
          } else {
            __DEV__ && warnInvalidSource(s)
          }
        })
    } else if (isFunction(source)) {
    // 监听的数据源是一个函数
      if (cb) {
        // doWatch 函数第二个参数 cb 有传入
        // watch 场景
        getter = () =>
        // 执行 source 函数将结果返回给 getter
          callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
      } else {
        // doWatch 函数 cb 没有传入
        // watchEffect 场景
        getter = () => {
        // 清除依赖
          if (cleanup) {
            cleanup()
          }
          // 执行 source 函数并返回给 getter
          return callWithAsyncErrorHandling(
            source,
            instance,
            ErrorCodes.WATCH_CALLBACK,
            [onCleanup],
          )
        }
      }
    }
// 递归读取响应式数据
    if (cb && deep) {
      const baseGetter = getter
      getter = () => traverse(baseGetter())
    }
    
// 定义清除副作用函数    
    let cleanup: (() => void) | undefined
    let onCleanup: OnCleanup = (fn: () => void) => {
      cleanup = effect.onStop = () => {
        callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
        cleanup = effect.onStop = undefined
      }
    }
}

job

为了控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数

// packages/runtime-core/src/apiWatch.ts

const job: SchedulerJob = () => {
  if (!effect.active || !effect.dirty) {
    return
  }
  if (cb) {
    // 存在 cb 表示处理 watch 的场景
    const newValue = effect.run() // 执行副作用函数获取最新值
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
        : hasChanged(newValue, oldValue)) || // 如果数据源是响应式数据、强制更新、新旧值发生了变化
      (__COMPAT__ &&
        isArray(newValue) &&
        isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
    ) {
      // 当回调再次执行前先清除副作用
      if (cleanup) {
        cleanup()
      }
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
    // 首次调用时,将 oldValue 的值设置为 undefined
        oldValue === INITIAL_WATCHER_VALUE
          ? undefined 
          : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
        onCleanup,
      ])
      oldValue = newValue // 更新旧值
    }
  } else {
    // watchEffect 的场景
    effect.run()
  }
}

scheduler

// packages/runtime-core/src/apiWatch.ts

job.allowRecurse = !!cb // 让调度器任务作为侦听器的回调以至于调度器能知道它可以被允许自己派发更新

let scheduler: EffectScheduler // 声明一个调度器
if (flush === 'sync') {
// 调度器函数同步执行,会立即被执行
  scheduler = job as any 
} else if (flush === 'post') {
// 调度器函数会将任务推到一个延时执行的微任务队列中,等待 DOM 更新结束后在执行
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // 默认情况: 'pre'
  job.pre = true
  if (instance) job.id = instance.uid
  scheduler = () => queueJob(job) // 将 job 函数推到一个优先执行时机的队列中
}

const effect = new ReactiveEffect(getter, NOOP, scheduler) // 创建副作用函数

if (cb) {
  if (immediate) {
  // 回调需要立即执行
    job()
  } else {
  // 手动调用副作用函数,将返回值赋值给旧值
    oldValue = effect.run()
  }
} else if (flush === 'post') {
// 将副作用函数放入到微任务队列中,等组件挂载完成后再执行副作用函数
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense,
  )
} else {
  effect.run()
}

unwatch

// packages/runtime-core/src/apiWatch.ts

const scope = getCurrentScope() // 获取当前的作用域

const unwatch = () => {
  effect.stop() // 停止追踪响应式数据的变化
  if (scope) {
    remove(scope.effects, effect) // 将 effect 从作用域列表中移除
  }
}

traverse

// packages/runtime-core/src/apiWatch.ts

// 递归遍历数组或对象
export function traverse(
  value: unknown,
  depth?: number, // 遍历的深度,表示函数在递归过程中应该达到的最大深度
  currentDepth = 0, // 当前递归的深度,用于追踪当前递归的深度
  seen?: Set<unknown>, // 表示已经遍历过的值的集合,用于检测循环引用,避免处理循环引用进入无限循环
) {
// value不是响应式对象或value被标记为SKIP直接返回,表示递归结束
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
// 控制递归深度
  if (depth && depth > 0) {
    if (currentDepth >= depth) {
      return value
    }
    currentDepth++
  }

// 处理遍历过的值
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, depth, currentDepth, seen)
  } else if (isArray(value)) {
  // 遍历数组,递归数组的每一项元素
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], depth, currentDepth, seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, depth, currentDepth, seen)
    })
  } else if (isPlainObject(value)) {
  // 遍历对象,递归对象的每一项属性
    for (const key in value) {
      traverse(value[key], depth, currentDepth, seen)
    }
  }
  return value
}