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函数,没有本质的区别。

相关推荐
customer0822 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
qq_3901617737 分钟前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test1 小时前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.2 小时前
Chrome调试工具(查看CSS属性)
前端·chrome