乐趣区

关于javascript:Vue3-源码解析十watch-的实现原理

本篇文章笔者会解说 Vue3 中侦听器相干的 api:watchEffect 和 watch。在 Vue3 之前 watch 是 option 写法中一个很罕用的选项,应用它能够十分不便的监听一个数据源的变动,而在 Vue3 中随着 Composition API 的写法推广也将 watch 独立成了一个 响应式 api,明天咱们就一起来学习 watch 相干的侦听器是如何实现的。

👇 储备常识要求:

在浏览本文前,倡议你曾经学习过本系列的第 7 篇文章的 effect 副作用函数的相干常识,否则在解说副作用的相干局部可能会呈现不了解的状况。

watchEffect

因为 watch api 中的许多行为都与 watchEffect api 统一,所以笔者将 watchEffect 放在首位解说,为了依据响应式状态主动利用和从新利用副作用,咱们能够应用 watchEffect 办法。它立刻执行传入的一个函数,同时响应式追踪其依赖,并在以来变更时从新运行该函数。

watchEffect 函数的实现十分简洁:

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

首先来看参数类型:

export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void

export interface WatchOptionsBase {
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: ReactiveEffectOptions['onTrack']
  onTrigger?: ReactiveEffectOptions['onTrigger']
}

export type WatchStopHandle = () => void

第一个参数 effect,接管函数类型的变量,并且在这个函数中会传入 onInvalidate 参数,用以革除副作用。

第二个参数 options 是一个对象,在这个对象中有三个属性,你能够批改 flush 来扭转副作用的刷新机会,默认为 pre,当批改为 post 时,就能够在组件更新后触发这个副作用侦听器,改同 sync 会强制同步触发。而 onTrack 和 onTrigger 选项能够用于调试侦听器的行为,并且两个参数只能在开发模式下工作。

参数传入后,函数会执行并返回 doWatch 函数的返回值。

因为 watch api 也会调用 doWatch 函数,所以 doWatch 函数的具体逻辑咱们会放在后边讲。先看 watch api 的函数实现。

watch

这个独立进去的 watch api 与组件中的 watch option 是齐全等同的,watch 须要侦听特定的数据源,并在回调函数中执行副作用。默认状况下这个侦听是惰性的,即只有当被侦听的源发生变化时才执行回调。

与 watchEffect 相比,watch 有以下不同:

  • 懒性执行副作用
  • 更具体地说明阐明状态应该处罚侦听器从新运行
  • 可能拜访侦听状态变动前后的值

watch 函数的函数签名有许多种重载状况,且代码行数较多,所以笔者不筹备剖析每个重载状况,一起来看一下 watch api 的实现。

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)
}

watch 接管 3 个参数,source 侦听的数据源,cb 回调函数,options 侦听选项。

source 参数

source 的类型如下:

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
type MultiWatchSources = (WatchSource<unknown> | object)[]

从两个类型定义看出,数据源反对传入单个的 Ref、Computed 响应式对象,或者传入一个返回雷同泛型类型的函数,以及 source 反对传入数组,以便能同时监听多个数据源。

cb 参数

在这个最通用的申明中,cb 的类型是 any,然而其实 cb 这个回调函数也有他本人的类型:

export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onInvalidate: InvalidateCbRegistrator
) => any

在回调函数中,会提供最新的 value、旧 value,以及 onInvalidate 函数用以革除副作用。

options

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}

能够看到 options 的类型 WatchOptions 继承了 WatchOptionsBase,这也就是 watch 除了 immediate 和 deep 这两个特有的参数外,还能够传递 WatchOptionsBase 中的所有参数以管制副作用执行的行为。

剖析完参数后,能够看到函数体内的逻辑与 watchEffect 简直统一,然而多了在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。

执行 doWatch 时的传参加 watchEffect 相比,多了第二个参数回调函数。

上面就让咱们揭开这个终极 boss doWatch 的庐山真面目吧。

doWatch

不论是 watchEffect、watch 还是组件内的 watch 选项,在执行时最终调用的都是 doWatch 中的逻辑,这个弱小的 doWatch 函数为了兼容各个 api 的逻辑源码也是挺长的大概有 200 行,所以老规矩,笔者会将长源码拆离开来讲。若想浏览残缺源码请戳这里。

先从 doWatch 的函数签名看起:

function doWatch(source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {immediate, deep, flush, onTrack, onTrigger}: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle

这个函数签名与 watch 基本一致,多了一个 instance 的参数,默认值为 currentInstance,currentInstance 是以后调用组件裸露进去的一个变量,不便该侦听器找到本人对应的组件。

而 source 在这里的类型就比拟清晰,反对单个的 source 或者数组,也只是一个一般对象。

接着会创立三个变量,getter 最终会当做副作用的函数参数传入,forceTrigger 标识是否须要强制更新,isMultiSource 标记传入的是单个数据源还是以数组模式传入的多个数据源。

let getter: () => any
let forceTrigger = false
let isMultiSource = false

而后会开始判断 source 的类型,依据不同的类型重置这三个参数的值。

  • ref 类型

    • 拜访 getter 函数会获取到 source.value 值,间接解包。
    • forceTrigger 标记会依据是否是 shallowRef 来设置。
  • reactive 类型

    • 拜访 getter 函数间接返回 source,因为 reactive 的值不须要解包获取。
    • 因为 reactive 中往往有多个属性,所以会将 deep 设置为 true,这里能够看出从内部给 reactive 设置 deep 是有效的。
  • 数组 array 类型

    • 将 isMultiSource 设置为 true。
    • forceTrigger 会依据数组中是否存在 reactive 响应式对象来判断。
    • getter 是一个数组模式,是 source 内各个元素的单个 getter 后果。
  • source 是函数 function 类型

    • 如果有回调函数

      • getter 就是 source 函数执行的后果,这种状况个别是 watch api 中的数据源以函数的模式传入。
    • 如果没有回调函数,那么此时就是 watchEffect api 的场景了。

      • 此时会为 watchEffect 设置 getter 函数,getter 函数逻辑如下:

        • 如果组件实例曾经卸载,则不执行,间接返回
        • 否则执行 cleanup 革除依赖
        • 执行 source 函数
  • 如果 source 不是以上的状况,则将 getter 设置为空函数,并且报出 source 不非法的正告⚠️。

相干代码如下,因为逻辑曾经残缺的一丝不落的在下面剖析了,所以就容笔者偷个懒,不加正文了。

if (isRef(source)) { // ref 类型的数据源,更新 getter 与 forceTrigger
  getter = () => (source as Ref).value
  forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) { // reactive 类型的数据源,更新 getter 与 deep
  getter = () => source
  deep = true
} else if (isArray(source)) { // 多个数据源,更新 isMultiSource、forceTrigger、getter
  isMultiSource = true
  forceTrigger = source.some(isReactive)
  // getter 会以数组模式返回数组中数据源的值
  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,让数据源作为 getter 函数
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    // 没有回调即为 watchEffect 场景
    getter = () => {if (instance && instance.isUnmounted) {return}
      if (cleanup) {cleanup()
      }
      return callWithAsyncErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onInvalidate]
      )
    }
  }
} else {
  // 其余状况 getter 为空函数,并收回正告
  getter = NOOP
  __DEV__ && warnInvalidSource(source)
}

接着会解决 watch 中的场景,当有回调,并且 deep 选项为 true 时,将应用 traverse 来包裹 getter 函数,对数据源中的每个属性递归遍历进行监听。

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

之后会申明 cleanup 和 onInvalidate 函数,并在 onInvalidate 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用,这些响应须要在其生效时革除,所以侦听副作用传入的函数能够接管一个 onInvalidate 函数作为入参,用来注册清理生效时的回调。当以下状况产生时,这个生效回调会被触发:

  • 副作用行将从新执行时。
  • 侦听器被进行(如果在 setup() 或生命周期钩子函数中应用了 watchEffect,则在组件卸载时)。
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {cleanup = runner.options.onStop = () => {callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

接着会初始化 oldValue 并赋值。

而后申明一个 job 函数,这个函数最终会作为调度器中的回调函数传入,因为是一个闭包模式依赖内部作用域中的许多变量,所以会放在前面讲,避免出现还未声明的变量造成了解艰难。

依据是否有回调函数,设置 job 的 allowRecurse 属性,这个设置很重要,可能让 job 作为一个观察者的回调这样调度器就能晓得它容许调用本身。

接着申明一个 scheduler 的调度器对象,依据 flush 的传参来确定调度器的执行机会。

  • 当 flush 为 sync 同步时,间接将 job 赋值给 scheduler,这样这个调度器函数就会间接执行。
  • 当 flush 为 post 须要提早执行时,将 job 传入 queuePostRenderEffect 中,这样 job 会被增加进一个提早执行的队列中,这个队列会在组件被挂载后、更新的生命周期中执行。
  • 最初是 flush 为默认的 pre 优先执行的状况,这是调度器会辨别组件是否曾经挂载,副作用第一次调用时必须是在组件挂载之前,而挂载后则会被推入一个优先执行机会的队列中。

这一部分逻辑的源码如下:

// 初始化 oldValue
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => { /* 临时疏忽逻辑 */} // 申明一个 job 调度器工作,临时不关注外部逻辑

// 重要:让调度器工作作为侦听器的回调以至于调度器能晓得它能够被容许本人派发更新
job.allowRecurse = !!cb

let scheduler: ReactiveEffectOptions['scheduler'] // 申明一个调度器
if (flush === 'sync') {scheduler = job as any // 这个调度器函数会立刻被执行} else if (flush === 'post') {
  // 调度器会将工作推入一个提早执行的队列中
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
    // 默认状况 'pre'
  scheduler = () => {if (!instance || instance.isMounted) {queuePreFlushCb(job)
    } else {
      // 在 pre 选型中,第一次调用必须产生在组件挂载之前
      // 所以这次调用是同步的
      job()}
  }
}

在解决完以上的调度器局部后,会开始创立副作用。

首先申明一个 runner 变量,它创立一个副作用并将之前解决好的 getter 函数作为副作用函数传入,并在副作用选项中设置了提早调用,以及设置了对应的调度器。

并通过 recordInstanceBoundEffect 函数将该副作用函数退出组件实例的的 effects 属性中,好让组件在卸载时可能被动得进行这些副作用函数的执行。

接着会开始解决首次执行副作用函数。

  • 如果 watch 有回调函数

    • 如果 watch 设置了 immediate 选项,则立刻执行 job 调度器工作。
    • 否则首次执行 runner 副作用,并将返回值赋值给 oldValue。
  • 如果 flush 的刷新机会是 post,则将 runner 放入提早机会的队列中,期待组件挂载后执行。
  • 其余状况都间接首次执行 runner 副作用。

最初 doWatch 函数会返回一个函数,这个函数的作用是进行侦听,所以大家在应用时能够显式的为 watch、watchEffect 调用返回值以进行侦听。

// 创立 runner 副作用
const runner = effect(getter, {
  lazy: true,
  onTrack,
  onTrigger,
  scheduler
})

// 将 runner 增加进 instance.effects 数组中
recordInstanceBoundEffect(runner, instance)

// 初始化调用副作用
if (cb) {if (immediate) {job() // 有回调函数且是 imeediate 选项的立刻执行调度器工作
  } else {oldValue = runner() // 否则执行一次 runner,并将返回值赋值给 oldValue
  }
} else if (flush === 'post') {
     // 如果调用机会为 post,则推入提早执行队列
  queuePostRenderEffect(runner, instance && instance.suspense)
} else {
  // 其余状况立刻首次执行副作用
  runner()}

// 返回一个函数,用以显式的完结侦听
return () => {stop(runner)
  if (instance) {remove(instance.effects!, runner)
  }
}

doWatch 函数到这里就全副运行结束了,当初所有的变量曾经申明结束,尤其是最初申明的 runner 副作用。咱们能够回过头看看被调用了屡次的 job 中到底做了什么。

调度器工作中做的事件逻辑比拟清晰,首先会判断 runner 副作用是否被停用,如果曾经被停用则立刻返回,不再执行后续逻辑。

之后辨别场景,通过是否存在回调函数判断是 watch api 调用还是 watchEffect api 调用。

如果是 watch api 调用,则会执行 runner 副作用,将其返回值赋值给 newValue,作为最新的值。如果是 deep 须要深度侦听,或者是 forceTrigger 须要强制更新,或者新旧值产生了扭转,这三种状况都须要触发 cb 回调,告诉侦听器产生了变动。在调用侦听器之前会先通过 cleanup 革除副作用,接着触发 cb 回调,将 newValue、oldValue、onInvalidate 三个参数传入回调。在回调触发后再去更新 oldValue 的值。

而如果没有 cb 回调函数,即为 watchEffect 的场景,此时调度器工作仅仅须要执行 runner 副作用函数就好。

job 调度器工作中的具体代码逻辑如下:

const job: SchedulerJob = () => {if (!runner.active) { // 如果副作用以停用则间接返回
    return
  }
  if (cb) {// watch(source, cb) 场景
    // 调用 runner 副作用获取最新的值 newValue
    const newValue = runner()
    // 如果是 deep 或 forceTrigger 或有值更新
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as any[])[i])
          )
        : hasChanged(newValue, oldValue))
    ) {
      // 当回调再次执行前先革除副作用
      if (cleanup) {cleanup()
      }
      // 触发 watch api 的回调,并将 newValue、oldValue、onInvalidate 传入
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        // 首次调用时,将 oldValue 的值设置为 undefined
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onInvalidate
      ])
      oldValue = newValue // 触发回调后,更新 oldValue
    }
  } else {
    // watchEffect 的场景,间接执行 runner
    runner()}
}

总结

在本文中,笔者给大家具体解说了 Vue3 中提供的 watch、watchEffect 两个 api 的实现,并且在组件的 option 选项中的 watch,其实也是通过 doWatch 函数来实现侦听的。在解说的过程中,咱们发现 Vue3 中的侦听器也是通过副作用来实现的,所以了解侦听器之前须要先理解透彻副作用到底做了什么。

咱们看到 watch、watchEffect 的背地都是调用并返回 doWatch 函数,笔者拆解剖析了 doWatch 函数,让读者可能分明的晓得 doWatch 每一行代码都做了什么,以便于当咱们的侦听器不如本人预期的工作时,能够从细节之处剖析起因,而不至于瞎猜瞎试。

最初,如果这篇文章可能帮忙到你理解更理解 Vue3 中的 watch 的原理以及它的工作形式,心愿能给本文点一个喜爱❤️。如果想持续追踪后续文章,也能够关注我的账号或 follow 我的 github,再次谢谢各位可恶的看官老爷。

退出移动版