vue3.x 响应式系统:核心AP I(watch、computed)

0 阅读8分钟

watch

watch 用于在响应式数据发生变化时,调用你提供的回调函数。它属于惰性侦听器:默认情况下,仅在侦听的数据实际改变时才会执行回调,而不是立即执行。

语法

watch(source, callback, options?)
  • source:侦听源,可以是:
    • 一个 ref(包括 computed 返回的 ref)
    • 一个 reactive 对象(会隐式开启深度侦听)
    • 一个 getter 函数() => x
    • 由上述类型组成的数组(同时侦听多个源)
    • 其他情况会发出警告。
  • callback:数据变化时的回调,接收 (newValue, oldValue, onCleanup)。如果不是回调函数,会发出警告,提示使用 watchEffect。
  • options:可选配置对象
    • immediate,设为 true 时,侦听器会立即执行一次回调
    • deep,默认值为 false,但当侦听源为 reactive 对象时默认为 true。设为 true 会递归遍历对象的所有嵌套属性,任何内部变化都会触发回调。
    • flush,控制回调的调用时机。
    • once,设为 true 时回调仅执行一次后自动停止侦听。

示例 监听 ref 基本类型

const count = ref(0);

const handleClick = () => {
  count.value++;
};

// 监听单个 ref
watch(count, (newVal, oldVal) => {
  console.log("info.count 值", newVal, oldVal);
});

watch(count,()=> {})count 是一个ref 对象,会创建一个 getter 函数,返回 count.value.

image.png

ReactiveEffect 创建了一个响应式副作用实例,用于追踪依赖变化并触发回调

当执行 effect.run() ,实际上是执行 getter = () => count.value,即读取 count.value,开始收集依赖。

image.png

侦听 reactive 对象

  1. 直接监听 reactive 对象会隐式开启深度监听。
  2. 新旧值引用相同的问题。
  3. 监听 reactive 对象的某个属性必须使用 getter。
const reactiveObj = reactive({
  age: 20,
  address: "shenzhen",
});

watch(reactiveObj, (newVal, oldVal) => {
  console.log("watch reactiveObj", newVal, oldVal);
});

image.png

执行 watch

image.png

执行 doWatch

image.png

执行 augmentJob

image.png

示例 监听 getter 函数

const count = ref(0);

watch(
  () => count.value,
  (newVal, oldVal) => {
    console.log("watch count", newVal, oldVal);
  }
);

image.png

示例 侦听多个源

使用数组同时侦听多个源,回调会收到新值和旧值的数组。

const count = ref(0)
const name = ref('Vue')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`${oldCount}->${newCount}, ${oldName}->${newName}`)
})

示例 批量更新

const count = ref(0);

const handleClick = () => {
  // 批量更新,触发 watch 一次
   count.value++;
   count.value++;
   count.value++;

};

watch(count, (newVal, oldVal) => {
  console.log("watch count 值", newVal, oldVal);
});

示例 异步更新

const count = ref(0);

const handleClick = () => {
  
  count.value++; // 触发一次
  
  // 异步更新
  setTimeout(() => {
    count.value++; // 触发一次
  }, 1000);
};

watch(count, (newVal, oldVal) => {
  console.log("watch count 值", newVal, oldVal);
});

示例 watch options 配置项

一、immediate 立即执行

watch(count, (newVal, oldVal) => {
  // 立即执行一次,newVal 是当前值,oldVal 为 undefined
}, { immediate: true })

二、deep 深层遍历

设为 true 会递归遍历对象的所有嵌套属性,任何内部变化都会触发回调。

const state = reactive({ count: 0, nested: { a: 1 } })
watch(() => state, (newVal, oldVal) => {
  // 任何深层变化都会触发
}, { deep: true })

三、once 只执行一次

Vue 3.4+ 支持,设为 true 时回调仅执行一次后自动停止侦听。

watch(count, callback, { once: true })

fluhs 实现机制

1、post

将 job 推入 queuePostFlushCb 队列。该队列会在组件更新完成后的微任务阶段执行,确保可以访问到最新渲染的 DOM。

2、默认 pre

将 job 推入 queueJob 队列。这是一个 异步微任务队列,并且会去重。多个 pre 类型的 watch 在同一轮事件循环中只会入队一次。这些 job 会在组件更新前被批量执行。

如果是首次执行,则是在 组件渲染前执行回调。

image.png

3、sync

将 scheduler 直接设置为需要执行的 job,不经过任何异步队列。

image.png

watchEffect

watchEffect 配置项只支持flush, 配置 deepimmediateonce 会发出警告,提示使用 wtach(source,callback,options) 签名语法。 watchEffect 在初始化时会立即执行一次,并在这个过程中完成依赖收集。

  • 执行时机:默认在组件更新前执行(即 flush: 'pre')。
  • 首次执行:创建后立即执行一次,自动收集依赖。
  • 后续触发:依赖变化后,副作用会在组件更新前(同一微任务中)执行。
  • 典型场景:大多数不需要访问更新后 DOM 的场景,且希望副作用在渲染前同步某些状态。

watchPostEffect

等价于 watchEffect(fn, { flush: 'post' })

  • 执行时机组件更新后执行(flush: 'post')。
  • 首次执行:创建后立即执行一次,自动收集依赖。
  • 后续触发:依赖变化后,副作用会等待组件更新完成后(DOM 已更新)再执行。
  • 典型场景:需要操作更新后的 DOM(如获取元素尺寸、滚动位置、焦点管理)。

watchSyncEffect

等价于 watchEffect(fn, { flush: 'sync' })

  • 执行时机依赖变化后立即同步执行flush: 'sync')。
  • 首次执行:创建后立即执行一次,自动收集依赖。
  • 后续触发:每当依赖变化,副作用同步执行,不经过任何异步队列(微任务或宏任务)。这可能会导致性能问题,因为无法批量处理多次变化。
  • 典型场景:极少使用,通常用于调试或需要立即同步到外部系统的逻辑

示例 watcheffect 的 flush 配置

执行顺序

  1. watchSyncEffect: 如果在侦听器中配置了 flush: 'sync',它的回调会同步立即执行。
  2. watchEffect (flush: 'pre') : 默认的侦听器,会在组件更新前执行回调。
  3. onBeforeUpdate: 组件DOM更新前的生命周期钩子被调用。
  4. watchPostEffect (flush: 'post')执行所有后置侦听器的回调。
  5. onUpdated: 组件DOM更新后的生命周期钩子被调用。
<template>
  <div>
    <p id="cdom">count 值 {{ count }}</p>
    <button @click="handleClick">点击</button>
  </div>
</template>

<script lang="ts" setup>
import { ref, watchEffect, watchPostEffect, watchSyncEffect, onUpdated } from "vue";
const count = ref(0);

const handleClick = () => {
  count.value++;
};

// 默认 pre
watchEffect(() => {
  console.log("Pre watchEffect count", count.value);
  const dom = document.getElementById("cdom") as HTMLElement;
  console.log("pre-dom", dom, dom?.textContent);
});

watchPostEffect(() => {
  console.log("watchPostEffect count", count.value);
  const dom = document.getElementById("cdom") as HTMLElement;
  console.log("post-dom", dom, dom?.textContent);
});

watchSyncEffect(() => {
  console.log("watchSyncEffect count", count.value);
  const dom = document.getElementById("cdom") as HTMLElement;
  console.log("syncdom", dom, dom?.textContent);
});

onUpdated(() => {
  console.log("onUpdated count", count.value);
  const dom = document.getElementById("cdom") as HTMLElement;
  console.log("update-dom", dom, dom?.textContent);
});
</script>

初始化

image.png

更新 count Ref

DOM 元素对象的引用是固定的,但它的 属性(例如 textContent)是可以被修改的。Vue 在组件更新时,会复用同一个 DOM 元素,只是更新其内容。

image.png

源码 effect

function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
  if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  const e = new ReactiveEffect(fn)
  if (options) {
    extend(e, options)
  }
  try {
    e.run() // 首次、立即执行一次
  } catch (err) {
    // 如果副作用函数执行过程中抛出异常,停止该副作用函数的依赖追踪
    e.stop()
    throw err
  }
  const runner = e.run.bind(e) as ReactiveEffectRunner
  runner.effect = e
  return runner
}

watch 源码

image.png

image.png

computed

在 Vue 3 的组合式 API 中,computed 是一个用于创建派生状态的核心函数。它允许你定义一个依赖于其他响应式数据(refreactive 等)的值,并且会自动追踪依赖、缓存计算结果,仅在依赖变化时才重新求值。

注意事项

  1. 计算属性必须是纯函数(无副作用)。Vue 的 ESLint 插件(eslint-plugin-vue)提供了规则 vue/no-side-effects-in-computed-properties禁止在计算属性的 getter 中修改外部状态、执行异步请求或操作 DOM
  2. 不要直接修改计算属性的值(只读场景)。
  3. 计算属性会缓存结果,但依赖必须是响应式的。

示例 只读计算属性

传入一个 getter 函数,返回一个只读的 ref 对象

<script setup lang="ts">
import { computed, ref } from "vue";
const color = ref(0);

const sub = computed(() => {
  return color.value;
});

console.log(sub.value);
</script>

image.png

image.png

初始化 ComputedRefImpl 实例时的 dep 属性。dep 管理计算属性自身的依赖,类型为 Dep

image.png

computed计算属性返回

image.png

示例 可写计算属性

传入一个带有 get 和 set 方法的对象,返回一个可读可写的 ref

const firstName = ref<string>("");
const lastName = ref<string>("");

const fullName = computed({
  get() {
    return firstName.value + " " + lastName.value;
  },
  set(newValue: string) {
    const [first = "", last = ""] = newValue.split(" ");
    firstName.value = first;
    lastName.value = last;
  },
});

fullName.value = "Li Hi"; //会触发 setter,更新 firstName 和 lastName

源码

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

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 创建计算属性实例
  const cRef = new ComputedRefImpl(getter, setter, isSSR)

  // 如果是开发环境,且提供了调试选项,将调试选项赋值给计算属性实例的 onTrack 和 onTrigger 属性
  if (__DEV__ && debugOptions && !isSSR) {
    cRef.onTrack = debugOptions.onTrack
    cRef.onTrigger = debugOptions.onTrigger
  }

  // 返回计算属性实例
  return cRef as any
}
class ComputedRefImpl<T = any> implements Subscriber {
  /**
   * @internal
   * 存储计算结果,初始值为 undefined
   */
  _value: any = undefined
  /**
   * @internal
   * 管理计算属性自身的依赖,类型为 Dep
   */
  readonly dep: Dep = new Dep(this)
  /**
   * @internal
   * 标记为响应式引用
   */
  readonly __v_isRef = true
  // TODO isolatedDeclarations ReactiveFlags.IS_REF
  /**
   * @internal
   */
  readonly __v_isReadonly: boolean
  // TODO isolatedDeclarations ReactiveFlags.IS_READONLY
  // A computed is also a subscriber that tracks other deps
  /**
   * @internal
   * 计算属性所依赖的其他响应式数据 链表头
   */
  deps?: Link = undefined
  /**
   * @internal
   * 计算属性所依赖的其他响应式数据 链表尾
   */
  depsTail?: Link = undefined
  /**
   * @internal
   * 状态标志,初始为 EffectFlags.DIRTY,表示需要重新计算
   */
  flags: EffectFlags = EffectFlags.DIRTY
  /**
   * @internal
   * 全局版本号,初始为 globalVersion - 1,用于优化计算
   */
  globalVersion: number = globalVersion - 1
  /**
   * @internal
   */
  isSSR: boolean
  /**
   * @internal
   * 指向下一个订阅者,用于批处理
   */
  next?: Subscriber = undefined

  // for backwards compat
  // 为了向后兼容,指向自身
  effect: this = this
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  /**
   * Dev only
   * @internal
   */
  _warnRecursive?: boolean

  constructor(
    // 作为实例属性
    public fn: ComputedGetter<T>,
    // 作为实例属性
    private readonly setter: ComputedSetter<T> | undefined,
    isSSR: boolean,
  ) {
    this[ReactiveFlags.IS_READONLY] = !setter
    this.isSSR = isSSR
  }

  /**
   * @internal
   */
  notify(): true | void {
    this.flags |= EffectFlags.DIRTY 

    if (
      !(this.flags & EffectFlags.NOTIFIED) &&
      // avoid infinite self recursion
      activeSub !== this
    ) {
      // 将计算属性加入计算批处理队列
      batch(this, true)
      // 返回 true,表示这是一个计算属性,需要通知其依赖
      return true
    } else if (__DEV__) {
      // TODO warn
    }
  }

  // 执行计算属性的 getter 函数
  get value(): T {
    // 记录依赖追踪信息
    const link = __DEV__
      ? this.dep.track({
          target: this,
          type: TrackOpTypes.GET,
          key: 'value',
        })
      : this.dep.track()

    // 刷新计算值
    refreshComputed(this)
    // sync version after evaluation
    if (link) {
      // 同步版本号
      link.version = this.dep.version
    }
    return this._value
  }

  // 执行计算属性的 setter 函数
  set value(newValue) {
    if (this.setter) {
      
      this.setter(newValue)
    } else if (__DEV__) {
      warn('Write operation failed: computed value is readonly')
    }
  }
}

最后