Vue 源码解析(四):watch

402 阅读6分钟

Vue watch 源码解析

原理综述

本文主要介绍watchwatchEffect两个API的实现原理,它们的原理可以简单地抽象为通过:

const effect = new ReactiveEffect(getter, scheduler)

来创建一个副作用,其中:

  • getter对于watch来说就是监听的数据源source对应的getter,对于watchEffect来说就是它所运行的副作用函数
  • scheduler可以简单的理解为是watch所传入的回调,或是watchEffect所运行的副作用函数,对应的是当数据源更改时,所需要执行的操作

因此,在watch初始化时通过effect.run收集依赖,当watchwatchEffect的依赖更改时,这些依赖触发effect,从而调用effect.scheduler方法,执行watch的回调或重新执行watchEffect传入的副作用。

关于watch的卸载原理,简单来说就是:在创建组件时,会同时创建一个effectScope,所有通过ReactiveEffect创建的副作用都会在constructor中通过recordEffectScopepusheffectScope当中,而组件在卸载时会调用effectScope.stop,从而停止该副作用域里面的所有副作用,自然也包括watchwatchEffect,于是就实现了侦听器的自动停止。

image.png

源码分析

watch API

懒监听 + 深度监听

watch API总共接受三个参数:监听的数据源source、数据源更改后执行的回调函数cb以及选项options,其中options中最为常用的属性为immediatedeep,分别表示立即执行深度监听watch默认懒监听和深度监听。

watch API会通过调用doWatch函数对数据源进行监听,并返回unwatch函数,用来取消监听。

doWatch函数会首先初始化gettergetter会被用于进行依赖收集,对于数据源是不同的类型会做不同的处理:

  • 如果数据源是ref,那么则追踪source.value
  • 如果是响应式对象,则追踪该对象,并通过设置deep=true强制深度追踪依赖
  • 如果是数组,则其中每一项都是数据源,因此通过map对于其中的每一项都进行对应的处理后得到getter
  • 如果是函数,根据是否有cb判断是watch还是watchEffect,若有cb说明是watch,因此getter就是source(只是使用了callWithErrorHandling进行了错误处理);若没有cb说明是watchEffect,因此getter就是其对应的副作用函数(除此之外额外添加了一些内容,不是很重要)
if (isRef(source)) {
  getter = () => source.value
  forceTrigger = isShallow(source)
} else if (isReactive(source)) {
  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)
}

在得到getter后,在deep=true的时候需要深度监听对象,因此需要得到深度遍历后的getter,所以调用traverse函数对getter中的对象进行深度遍历。traverse函数通过递归调用对对象进行深度访问,从而可以进行深度的依赖收集

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

// traverse 函数,代码太长不用看,就是用来对对象进行深度访问的,这样才能进行依赖收集
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
}

得到getter之后,我们使用getterscheduler得到effect,之后会调用effect.run(也就是调用getter函数),进行依赖的收集(scheduler放在这部分的最后讲)

const effect = new ReactiveEffect(getter, scheduler)

// initial run
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函数用来取消这个watchunwatch函数也很简单,就是调用副作用的stop方法,并且使用remove函数将这个副作用从所在实例(一般是组件实例)的作用域中移除,至此整体的逻辑完成

const unwatch = () => {
  effect.stop()
  if (instance && instance.scope) {
    remove(instance.scope.effects!, effect)
  }
}

return unwatch

scheduler的内容(也就是new ReactiveEffect的第二个参数)比较长,所以放在这一部分的最后来说。

默认情况下,flush==='pre',因此scheduler函数就是把job回调函数放在队列中,等待在渲染前的时间节点统一执行。所以当调用effect.scheduler的时候实际上只是调度了job而非执行

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'
  job.pre = true
  if (instance) job.id = instance.uid
  scheduler = () => queueJob(job)
}

那么,job函数的任务是什么呢?

其实代码虽多,最核心的工作就是通过调用callWithAsyncErrorHandling函数来调用cb,也就是watch的回调,并更新执行后的oldValue;对于watchEffect的情况,实际上只需要重新执行副作用,因此调用effect.run方法

那么为什么对于watch的情况需要做那么多的判断呢,这里设计者应该是有他的考量,他似乎不希望watch深度监听类似于ref({ count: 0 })的情况,这种情况下,只有ref本身的引用(也就是.value)发生改变才会导致回调触发,但是我也不清楚为什么要这样做

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
        : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
        ? []
        : oldValue,
        onCleanup
      ])
      oldValue = newValue
    }
  } else {
    // watchEffect
    effect.run()
  }
}

立即监听

对于立即监听的情况,实际上是希望在创建watch时能够立即调用一次回调,那么我们只需要立即执行一次job函数即可

// initial run
if (cb) {
  if (immediate) {
    job() // 立即监听
  } else {
    oldValue = effect.run()
  }
} else if (flush === 'post') {
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense
  )
} else {
  effect.run()
}

浅监听

如果需要浅监听而非深度监听,只需要不执行traverse的过程即可

if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter()) // deep=false,不会执行深度依赖收集
}

watchEffect API

原理

其实原理,上面讲watch的时候已经带到了,这里简单总结他的过程,相比于watch简单很多:

  • 创建effectgetter就是watchEffect的副作用,scheduler就是把effect.run放进队列中等待调度,因此两者是相似的
  • 创建后会立即通过effect.run()收集依赖,同时也会执行副作用
  • 依赖触发过程会调用effect.scheduler,实际上就是effect.run,因此会重新执行副作用

和 effect 函数的对比

watchEffect这个API其实和Vue内置的effect函数功能很像,都是立即执行一个副作用函数,当依赖改变时副作用会重新执行。主要区别在于:

  • watchEffect可以通过设置flush控制副作用的触发时机,例如默认(flush: pre)是在组件渲染前执行,通过设置flush: post则可以推迟到组件渲染后;而effect函数则是在依赖更改后立即触发副作用,无法进行控制
  • effect不是Vue暴露的API,不用于生产当中

值得注意的是,不同于watch,这里收集依赖并不是深度的,这一点和effect是相同的。

const obj = reactive({ count: 0 })

watchEffect(() => {
  console.log(obj)
})

obj.count += 1 // 并不会触发 watchEffect