AI 生成的摘要
本文介绍了 Vue.js 中的两个核心功能:`computed` 和 `watch`。`computed` 用于创建响应式的计算属性,支持只读和可写两种模式。其核心实现通过封装 `getter` 函数,基于依赖追踪机制在响应式数据变化时动态计算最新值。`watch` 则用于监听响应式数据源的变动,并在变动时执行回调函数。支持的特性包括对单一或多个来源的监听、深度监听、立即执行选项以及在数据变化或监听停止时执行的清理函数。文章详细介绍了这两种功能的实现细节,并通过 TypeScript 和面向对象编程思维,构建了对这两者的扩展和优化。提供的代码例子涵盖了如何实现响应式数据的自动依赖追踪与更新,帮助开发者更好地控制 Vue 应用中的数据流和状态管理。

computed

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

ts
// 只读
function computed<T>(
  getter: () => T,
  // 参见下面的"Computed 调试"链接
  debuggerOptions?: DebuggerOptions,
): Readonly<Ref<Readonly<T>>>

// 可写
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions,
): Ref<T>

computed 有两个签名,可读和可写。

支持get

ts
import type { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { type Ref, trackRefValue, triggerRefValue } from './ref'

declare const ComputedRefSymbol: unique symbol

export interface ComputedRef<T = any> extends Ref {
  readonly value: T
  [ComputedRefSymbol]: true
}

type ComputedGetter<T> = (...args: any[]) => T

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

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

  public readonly __v_isRef = true
  public _dirty = true

  constructor(getter: ComputedGetter<T>) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }

  set value(_newValue: T) {}
}

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T> {
  return new ComputedRefImpl(getter) as any
}

当组件中读取计算属性时,会触发 get value(),调用trackRefValue收集依赖,_dirty 属性用来标记属性是否是脏值(脏值不需要重新计算),第一次访问时_dirty肯定是 false,于是就会触发this.effect.run(),而在 constructor中,this.effect是 ReactiveEffect 传入getter(computed 传入的函数)以及:

ts
// 我是scheduler
() => {
  if (!this._dirty) {
    this._dirty = true
    triggerRefValue(this)
  }
}
ReactiveEffect.ts
ts
export class ReactiveEffect<T = any> {

  // fn 指 updateComponent()
  constructor(public fn: () => T, public scheduler?: EffectScheduler | null) {}

  run() {
    let parent: ReactiveEffect | undefined = activeEffect
    activeEffect = this
    const res = this.fn()
    activeEffect = parent
    return res
  }
}

调用this.effect.run时,activeEffect 为 this.effect,那么执行getter时,其中如果有响应式数据,就会将 activeEffect 收集到 Dep 中,每当其中的响应式数据更新时,会触发set,set 会遍历 dep(其中就包括这个的 computed 的 effect)并执行他的scheduler(ReactiveEffect的第二个函数)。scheduler 中如果_dirty是false(已经有值且标记为不需要更新),那么就会设置_dirty为 true,并执行triggerRefValue。对应的组件 effect 会被触发,就会重新访问get value(),这时就会触发重新计算。

支持 set

ts
import { isFunction } from '../shared'
import type { Dep } from './dep'
import { ReactiveEffect } from './effect'
import { type Ref, trackRefValue, triggerRefValue } from './ref'

declare const ComputedRefSymbol: unique symbol

export interface ComputedRef<T = any> extends Ref {
  readonly value: T
  [ComputedRefSymbol]: true
}

export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

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

  public readonly __v_isRef = true
  public _dirty = true

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    trackRefValue(this)
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }

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

export function computed<T>(getterOrOptions: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(getterOrOptions: WritableComputedOptions<T>): Ref<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)

  if (onlyGetter) {
    getter = getterOrOptions
    setter = () => {}
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(getter, setter) as any
}

watch

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

ts
// 侦听单个来源
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): WatchHandle

// 侦听多个来源
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): WatchHandle

type WatchCallback<T> = (
  value: T,
  oldValue: T,
  onCleanup: (cleanupFn: () => void) => void
) => void

type WatchSource<T> =
  | Ref<T> // ref
  | (() => T) // getter
  | (T extends object ? T : never) // 响应式对象

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // 默认:false
  deep?: boolean | number // 默认:false
  flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  once?: boolean // 默认:false (3.4+)
}

interface WatchHandle {
  (): void // 可调用,与 `stop` 相同
  pause: () => void
  resume: () => void
  stop: () => void
}

使用 ReactiveEffect 就可以快速实现

ts
import { ReactiveEffect } from "../reactivity"

export type WatchEffect = (onCleanup: OnCleanup) => void

export type WatchSource<T = any> = () => T

type OnCleanup = (cleanupFn: () => void) => void

export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  let oldValue: T
  const effect = new ReactiveEffect(() => {
    const newValue = source()
    cb(newValue, oldValue)
    oldValue = newValue
  }, () => {
    const newValue = source()
    cb(newValue, oldValue)
    oldValue = newValue
  })
  effect.run()
}

优化:

ts
export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  const getter = () => source()
  let oldValue = getter()
  const job = () => {
    const newValue = getter()
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }

  const effect = new ReactiveEffect(getter, job)
  effect.run()
}

然后在此基础上继续拓展,支持 Ref:

supportRef
ts
export type WatchSource<T = any> = () => T | Ref<T>

type OnCleanup = (cleanupFn: () => void) => void

export function watch<T>(
  source: WatchSource<T>,
  cb: (newValue: T, oldValue: T) => void,
) {
  const getter = () => {
    const value = source()
    return isRef(value) ? value.value : value
  }
  let oldValue = getter()
  const job = () => {
    const newValue = getter()
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }

  const effect = new ReactiveEffect(getter, job)
  effect.run()
}

支持数组类型的 source

supportArraySource
ts
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
) {
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

  let oldValue = getter()
  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  effect.run()
}

支持 immediate

supportImmediate
ts
export type WatchSource<T = any> = Ref<T> | (() => T)

type OnCleanup = (cleanupFn: () => void) => void

interface WatchOptions {
  immediate?: boolean
  deep?: boolean | number
}

export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions,
) {
  const { immediate = false } = options || {}
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  let oldValue: any
  if (immediate) {
    // job中会调用一次effect.run拿值,所以不用再次调用了
    job()
  } else {
    oldValue = effect.run()
  }
}

支持 deep

supportDeep
ts
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions,
) {
  const { immediate = false, deep = false } = options || {}
  let getter: () => any
  let isMultiSource = false
  if (isFunction(source)) {
    getter = source as () => any
  } else if (isRef(source)) {
    getter = () => source.value
  } else if (Array.isArray(source)) {
    isMultiSource = true
    getter = () => {
      return source.map(s => {
        if (isRef(s)) {
          return s.value
        }
        return s()
      })
    }
  } else {
    getter = () => source
  }

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

  const job = () => {
    const newValue = effect.run()
    if (isMultiSource
      ? (newValue as any[]).some((v, i) =>
          hasChanged(v, (oldValue as T[])?.[i]),
        )
      : hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  const effect = new ReactiveEffect(getter, job)
  let oldValue: any
  if (immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
}

export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value)) return value

  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (Array.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[key], seen)
    }
  }
  return value
}

watchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

supportWatchEffect
ts
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[]) => void,
  options?: WatchOptions
) {
  doWatch(source, cb, options);
}

export function watchEffect(source: WatchEffect) {
  doWatch(source, null)
}

function doWatch<T>(
  source: WatchSource<T> | WatchSource<T>[] | WatchEffect,
  cb: null | ((newValue: T | T[], oldValue: T | T[]) => void),
  options?: WatchOptions
) {
  const { immediate = false, deep = false } = options || {};
  let getter: () => any;
  let isMultiSource = false;
  if (isFunction(source)) {
    getter = source as () => any;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (Array.isArray(source)) {
    isMultiSource = true;
    getter = () => {
      return source.map((s) => {
        if (isRef(s)) {
          return s.value;
        }
        return s();
      });
    };
  } else {
    getter = () => source;
  }

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

  const job = () => {
    const newValue = effect.run();
    if (
      isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as T[])?.[i])
          )
        : hasChanged(newValue, oldValue)
    ) {
      cb?.(newValue, oldValue);
      oldValue = newValue;
    }
  };
  const effect = new ReactiveEffect(getter, job);
  let oldValue: any;
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }
}

支持 onCleanup

首先,拓展 ReactiveEffect,使其支持 stop,然后watch 的 callback 增加onCleanup参数,他接受一个函数,并将其保存起来,他会在 stop 执行时(也就是 watch return 出的函数被执行时)或者source 变化时,下一次执行 callback 之前触发:

apiWatch.ts
ts
export function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  cb: (newValue: T | T[], oldValue: T | T[], cleanup: OnCleanup) => void,
  options?: WatchOptions
) {
  return doWatch(source, cb, options);
}

export function watchEffect(source: WatchEffect) {
  return doWatch(source, null)
}

function doWatch<T>(
  source: WatchSource<T> | WatchSource<T>[] | WatchEffect,
  cb: null | ((newValue: T | T[], oldValue: T | T[], cleanup: OnCleanup) => void),
  options?: WatchOptions
) {
  const { immediate = false, deep = false } = options || {};
  let getter: () => any;
  let isMultiSource = false;
  if (isFunction(source)) {
    getter = source as () => any;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (Array.isArray(source)) {
    isMultiSource = true;
    getter = () => {
      return source.map((s) => {
        if (isRef(s)) {
          return s.value;
        }
        return s();
      });
    };
  } else {
    getter = () => source;
  }

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

  let cleanup: () => void
  let onCleanup: OnCleanup = (fn: () => void) => {
    cleanup = effect.onStop = () => {
      fn()
    }
  }

  const job = () => {
    const newValue = effect.run();
    if (
      isMultiSource
        ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as T[])?.[i])
          )
        : hasChanged(newValue, oldValue)
    ) {
    if (cleanup) {
        cleanup()
    }
      cb?.(newValue, oldValue, onCleanup);
      oldValue = newValue;
    }
  };
  const effect = new ReactiveEffect(getter, job);
  let oldValue: any;
  if (immediate) {
    job();
  } else {
    oldValue = effect.run();
  }

  const unwatch = () => {
    effect.stop()
  }
  return unwatch
}