watch watchEffect computed是怎么实现的

前言

这是vue3系列源码的第九章,使用的vue3版本是3.4.15

背景

前面我们看了vue3源码的,渲染,更新过程以及响应式的实现还通过一个特殊的案例看了props 的实现,这里看一下我们常用的监听功能的实现,也就是watch watchEffect computed函数的实现

前置

我们随便写一点内容,主要为了看一下这三个函数在源码中是如何实现的。

js 复制代码
<template>
  <div>{{ aa }}</div>
  <div>{{ bb }}</div>
  <button @click="aa = '小石'">点击</button>
 </template>
 <script setup>
 import { ref, watch, watchEffect, computed } from 'vue'
 
 const aa = ref('小石')

 const bb = computed(() => aa.value + '谭记')

 watch(aa, () => {console.log(11, aa.value)})
 
 watchEffect(() => {console.log(22, aa.value)})

 </script>

computed

首先看一下computed的实现。

ts 复制代码
const computed: typeof _computed = (
  getterOrOptions: any,
  debugOptions?: any,
) => {
  return _computed(getterOrOptions, debugOptions, isInSSRComponentSetup)
}

这里是调用了**_computed** 函数,如下:

ts 复制代码
function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

最终得到的是new ComputedRefImpl得到的对象。

ComputedRefImpl

ts 复制代码
class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false

  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean,
  ) {
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      () =>
        triggerRefValue(
          this,
          this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
            ? DirtyLevels.MaybeDirty_ComputedSideEffect
            : DirtyLevels.MaybeDirty,
        ),
    )
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (
      (!self._cacheable || self.effect.dirty) &&
      hasChanged(self._value, (self._value = self.effect.run()!))
    ) {
      triggerRefValue(self, DirtyLevels.Dirty)
    }
    trackRefValue(self)
    if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
      __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
      triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }

  // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
  get _dirty() {
    return this.effect.dirty
  }

  set _dirty(v) {
    this.effect.dirty = v
  }
  // #endregion
}

ComputedRefImpl这个类中,

  • new ReactiveEffect得到一个副作用对象,ReactiveEffect类前面我们分析过,是vue3响应式的核心类。
  • get 的时候,先运行effect.run,最终就是执行了() => aa.value + '谭记'得到了bb 的值,这里会触发aaget把当前的effect 加入到aaeffectdep中。同时,检查当前的值有没有变化,如何变了,会调用triggerRefValue 去更新相应的组件,最后执行了trackRefValue ,也就是绑定副作用函数或者称他为依赖收集
  • set 的时候,会执行传入进来的set函数,这里面我们没有传。

aa 的值发生变化的时候,会在aadep中找到这个effect ,并调用trigger,也就是这里传给triggerRefValue函数的第二个参数,也就是triggerRefValue 函数,也就是对依赖了computed的部分进行更新。

watch

下面看一下watch函数的实现

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

doWatch

ts 复制代码
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, once, onTrack, onTrigger, }: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
  if (cb && once) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      unwatch()
    }
  }

  const instance = currentInstance
  const reactiveGetter = (source: object) =>
    deep === true
      ? source // traverse will happen in wrapped getter below
      : // for deep: false, only traverse root-level properties
        traverse(source, deep === false ? 1 : undefined)

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

  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } 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) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup],
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

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

  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
    if (!effect.active || !effect.dirty) {
      return
    }
    if (cb) {
      // watch(source, cb)
      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))
      ) {
        // 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()
    }
  }

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

  const effect = new ReactiveEffect(getter, NOOP, scheduler)

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

  // 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()
  }
  return unwatch
}

doWatch主要干了这几件事情:

  • 首先根据source 的类型,来确定getter 的值,我们这里是ref,所以就是source.value,source 的值还可以是reactive,array,function
  • watch 函数第三个参数可以传入这些字段{ immediate, deep, flush, once, onTrack, onTrigger},如果deeptrue,会对我们传入的source进行递归,深度监听
  • 接着定义了任务函数,这里会根据是否有cb ,来判断调用的是watch 还是watchEffect
  • 然后根据任务的执行时机,添加到不同的任务队列中,这里默认是pre ,加入到queueJob队列中
  • 根据getter任务函数创建副作用函数
  • 如果定义了immediate ,那么会立即执行一次回调,否则也会执行一次getter ,把当前的值作为oldValue ,那么在此时又会触发aaget ,把aa 作为这个watch 的依赖进行收集,或者说把这个watch 添加到aa的副作用函数的通知队列中

getter 发生变化的时候,会执行scheduler ,也就是定义的任务函数job ,也就是我们传入的回调函数cb

watchEffect

ts 复制代码
function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase,
): WatchStopHandle {
  return doWatch(effect, null, options)
}

watchEffect 走的也是doWatch ,和watch 没有本质的区别,这里的source会是一个函数,会直接执行,这个函数来进行依赖收集。

以上就是watch watchEffect compute的全部内容

总结

computed 每次读取数据的时候会先判断是否发生了值的变化,而且它返回的是和ref有点像的值。他的优点就是每次读取的时候会比较其依赖的响应式对象的值,如果没有变化,就不会触发更新流程。

watch watchEffect 都是调用的doWatch函数,没有本质的区别。

相关推荐
C语言魔术师13 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳1 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?1 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研9 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
轻口味9 小时前
Vue.js 组件之间的通信模式
vue.js
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae