普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月27日首页

Vue3 响应式系统核心执行器:Reactive Effect 与依赖调度机制

作者 excel
2025年9月27日 07:13

```js
 ┌─────────────────┐
 │ reactive.ts     │   ← 负责对象/数组/Ref 的响应式代理
 │ - track()   → 收集依赖
 │ - trigger() → 触发更新
 └─────────┬───────┘
           │ 调用
           ▼
 ┌─────────────────┐
 │ dep.ts          │   ← 管理单个依赖 Dep(存放订阅它的 effect)
 │ - Link 双向链表(Dep.deps 与 Effect.deps 双向绑定)
 │ - addSub()     → 添加 effect
 │ - removeSub()  → 移除 effect
 └─────────┬───────┘
           │ 调用
           ▼
 ┌───────────────────────────────────────────────┐
 │ effect.ts                                      │
 │   核心:ReactiveEffect 类,封装副作用函数逻辑     │
 │                                                │
 │ ReactiveEffect 类                               │
 │  - run()             → 执行副作用函数,触发依赖收集 │
 │  - trigger()         → 外部触发更新,决定是否重新运行 │
 │  - notify()          → 将 effect 加入批处理队列     │
 │  - stop()            → 停止 effect,解绑所有依赖     │
 │  - pause()/resume()  → 暂停/恢复依赖收集            │
 │                                                │
 │ 工具函数                                         │
 │  - prepareDeps()     → 标记当前依赖,等待收集       │
 │  - cleanupDeps()     → 清理失效的依赖关系          │
 │  - cleanupEffect()   → 执行注册的清理回调函数       │
 │  - isDirty()         → 判断 computed 是否过期       │
 │  - removeSub()       → 从 dep 移除 effect           │
 │  - removeDep()       → 从 effect 移除 dep           │
 │                                                │
 │ 调度机制                                         │
 │  - batch() / endBatch() → 批量执行 effect           │
 │  - runIfDirty()       → 脏检查后决定是否执行        │
 │                                                │
 │ 追踪控制                                         │
 │  - pauseTracking()    → 暂停依赖收集               │
 │  - enableTracking()   → 开启依赖收集               │
 │  - resetTracking()    → 恢复追踪栈状态             │
 │                                                │
 │ 外部接口                                         │
 │  - effect(fn)         → 创建并运行 ReactiveEffect   │
 │  - stop(runner)       → 停止某个 effect             │
 │  - onEffectCleanup(fn)→ 注册 effect 销毁时回调      │
 └─────────┬─────────────────────────────────────┘
           │ 关联
           ▼
 ┌──────────────────────────┐
 │ computed.ts              │ ← 基于 effect 实现的特殊 effect
 │ - ComputedRefImpl        → 使用 ReactiveEffect 封装 getter
 │ - refreshComputed()      → 调用 effect.run() 刷新缓存
 └──────────────────────────┘

```js
// -----------------------------
// Imports / Types
// -----------------------------

// EN: import helper utilities: `extend` to copy properties, `hasChanged` to compare values.
// CN: 导入工具函数:`extend` 用于合并/复制属性,`hasChanged` 用于比较值是否变化。
import { extend, hasChanged } from '@vue/shared'

// EN: import the ComputedRefImpl type to allow refreshComputed to access internals.
// CN: 导入 ComputedRefImpl 类型,使 refreshComputed 能访问 computed 的内部字段。
import type { ComputedRefImpl } from './computed'

// EN: import types for track/trigger op kinds (used in debugger event types).
// CN: 导入 track/trigger 操作类型,用于调试器事件类型定义。
import type { TrackOpTypes, TriggerOpTypes } from './constants'

// EN: Link type and globalVersion used by dep implementation.
// CN: Link 类型与 globalVersion,供 dep 实现使用。
import { type Link, globalVersion } from './dep'

// EN: activeEffectScope is used to register created effects to a scope for cleanup.
// CN: activeEffectScope 用于将创建的 effect 注册到当前的 effectScope,便于后续集中清理。
import { activeEffectScope } from './effectScope'

// EN: warn helper for dev-only warnings.
// CN: 开发环境使用的警告函数。
import { warn } from './warning'

// -----------------------------
// Type aliases & interfaces
// -----------------------------

// EN: Scheduler type for custom scheduling of effect runs.
// CN: effect 调度器类型,允许用户自定义何时执行 effect。
export type EffectScheduler = (...args: any[]) => any

// EN: Debugger event contains the effect (subscriber) plus extra info about the operation.
// CN: 调试事件,包含 effect(subscriber)以及关于操作的额外信息。
export type DebuggerEvent = { effect: Subscriber } & DebuggerEventExtraInfo

export type DebuggerEventExtraInfo = {
  target: object
  type: TrackOpTypes | TriggerOpTypes
  key: any
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

// EN: Debugger hooks for track/trigger events.
// CN: 调试选项:在 track/trigger 时会调用的钩子。
export interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

// EN: Options for ReactiveEffect: scheduler, allowRecuse, onStop, plus debugger hooks.
// CN: ReactiveEffect 可配置项:scheduler、allowRecurse、onStop,以及调试钩子。
export interface ReactiveEffectOptions extends DebuggerOptions {
  scheduler?: EffectScheduler
  allowRecurse?: boolean
  onStop?: () => void
}

// EN: Runner function returned by `effect`, with a reference to its effect instance.
// CN: `effect` 返回的 runner 函数类型,同时包含 `.effect` 指向 ReactiveEffect 实例。
export interface ReactiveEffectRunner<T = any> {
  (): T
  effect: ReactiveEffect
}

// EN: The active subscriber (effect/computed) at runtime.
// CN: 当前处于活跃状态的订阅者(可能是 effect 或 computed)。
export let activeSub: Subscriber | undefined

// -----------------------------
// Effect Flags (bitmask) - compact state flags
// -----------------------------

// EN: Bit flags to track effect/computed state efficiently.
// CN: 使用位掩码表示 effect 的各种状态,便于快速判断与组合状态。
export enum EffectFlags {
  /** ReactiveEffect only */
  ACTIVE = 1 << 0,
  RUNNING = 1 << 1,
  TRACKING = 1 << 2,
  NOTIFIED = 1 << 3,
  DIRTY = 1 << 4,
  ALLOW_RECURSE = 1 << 5,
  PAUSED = 1 << 6,
  EVALUATED = 1 << 7,
}

// -----------------------------
// Subscriber interface (internal)
// -----------------------------

// EN: Subscriber represents anything that subscribes to deps (effects, computed).
// CN: Subscriber 表示订阅依赖的实体(例如 effect 或 computed)。
export interface Subscriber extends DebuggerOptions {
  /** Head of the doubly linked list representing the deps @internal */
  deps?: Link
  /** Tail of the same list @internal */
  depsTail?: Link
  /** @internal */
  flags: EffectFlags
  /** @internal */
  next?: Subscriber
  /** returning `true` indicates it's a computed that needs to call notify on its dep too @internal */
  notify(): true | void
}

// -----------------------------
// ReactiveEffect class
// -----------------------------

// EN: A set to collect effects that were paused and should be triggered once resumed.
// CN: 用于缓存处于 PAUSED 状态且等待恢复后触发的 effects 的集合(弱引用,方便 GC)。
const pausedQueueEffects = new WeakSet<ReactiveEffect>()

// EN: ReactiveEffect implements Subscriber and holds effect runtime data and methods.
// CN: ReactiveEffect 实现 Subscriber,保存运行时数据与方法(run/stop/trigger 等)。
export class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
  /** @internal */ deps?: Link = undefined
  /** @internal */ depsTail?: Link = undefined
  /** @internal */ flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
  /** @internal */ next?: Subscriber = undefined
  /** @internal */ cleanup?: () => void = undefined

  // EN: optional scheduler and lifecycle/debug hooks
  // CN: 可选的调度器与生命周期/调试钩子
  scheduler?: EffectScheduler = undefined
  onStop?: () => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void

  // EN: constructor registers effect to active effect scope if any, and stores fn.
  // CN: 构造函数:如存在 activeEffectScope,会将该 effect 注册进去;并保存传入的 fn。
  constructor(public fn: () => T) {
    if (activeEffectScope && activeEffectScope.active) {
      activeEffectScope.effects.push(this)
    }
  }

  // EN: Mark the effect as paused (it will not run immediately on trigger).
  // CN: 将 effect 标记为 PAUSED(触发时不会立即运行,改为放入 pausedQueueEffects 中)。
  pause(): void {
    this.flags |= EffectFlags.PAUSED
  }

  // EN: Resume the effect: if it was queued while paused, run it now.
  // CN: 恢复 effect;若此前因为暂停被加入 pausedQueueEffects,会立刻触发一次。
  resume(): void {
    if (this.flags & EffectFlags.PAUSED) {
      this.flags &= ~EffectFlags.PAUSED
      if (pausedQueueEffects.has(this)) {
        pausedQueueEffects.delete(this)
        this.trigger()
      }
    }
  }

  /** @internal */
  // EN: notify is called by deps to schedule this effect for execution.
  // CN: notify 由 dep 调用以安排 effect 的执行(通过批处理机制或 scheduler)。
  notify(): void {
    if (
      this.flags & EffectFlags.RUNNING &&
      !(this.flags & EffectFlags.ALLOW_RECURSE)
    ) {
      // EN: if currently running and recursion not allowed, skip.
      // CN: 若正在运行且不允许递归,则忽略这次通知,避免无限递归。
      return
    }
    if (!(this.flags & EffectFlags.NOTIFIED)) {
      // EN: mark as notified and add to batch queue
      // CN: 标记为 NOTIFIED 并加入批处理队列
      batch(this)
    }
  }

  // EN: Run the effect function immediately (collect deps). Returns the result of fn().
  // CN: 立即执行 effect 函数并收集依赖,返回 fn 的执行结果。
  run(): T {
    // TODO cleanupEffect

    // EN: if effect is not active (stopped), still run fn but without tracking.
    // CN: 若 effect 已停止(ACTIVE 标志清除),则直接运行 fn(不进行依赖收集)。
    if (!(this.flags & EffectFlags.ACTIVE)) {
      // stopped during cleanup
      return this.fn()
    }

    // EN: mark running, prepare and cleanup deps before/after run
    // CN: 标记为 RUNNING,清理上次遗留的 effect 外部状态,准备 deps 以便收集。
    this.flags |= EffectFlags.RUNNING
    cleanupEffect(this)
    prepareDeps(this)
    const prevEffect = activeSub
    const prevShouldTrack = shouldTrack
    activeSub = this
    shouldTrack = true

    try {
      return this.fn()
    } finally {
      // EN: restore active effect and tracking state; cleanup any deps marked unused.
      // CN: 恢复先前的 activeSub 与 shouldTrack,清理未被使用的 deps,并清除 RUNNING 标记。
      if (__DEV__ && activeSub !== this) {
        warn(
          'Active effect was not restored correctly - ' +
            'this is likely a Vue internal bug.',
        )
      }
      cleanupDeps(this)
      activeSub = prevEffect
      shouldTrack = prevShouldTrack
      this.flags &= ~EffectFlags.RUNNING
    }
  }

  // EN: stop the effect and remove it from all deps; call onStop hook.
  // CN: 停止 effect:从所有 dep 中移除自己,执行 cleanup 与 onStop 回调,并清除 ACTIVE 标志。
  stop(): void {
    if (this.flags & EffectFlags.ACTIVE) {
      for (let link = this.deps; link; link = link.nextDep) {
        removeSub(link)
      }
      this.deps = this.depsTail = undefined
      cleanupEffect(this)
      this.onStop && this.onStop()
      this.flags &= ~EffectFlags.ACTIVE
    }
  }

  // EN: when triggered, either queue (if paused), call scheduler, or runIfDirty.
  // CN: trigger 时的行为:若 PAUSED,则排队;若有 scheduler 用 scheduler;否则按脏检查执行。
  trigger(): void {
    if (this.flags & EffectFlags.PAUSED) {
      pausedQueueEffects.add(this)
    } else if (this.scheduler) {
      this.scheduler()
    } else {
      this.runIfDirty()
    }
  }

  /** @internal */
  // EN: run effect only if it's considered dirty by `isDirty`.
  // CN: 仅当 isDirty 返回 true(依赖发生变化或版本不同等)时调用 run。
  runIfDirty(): void {
    if (isDirty(this)) {
      this.run()
    }
  }

  // EN: query property to check if effect is dirty.
  // CN: 通过 isDirty 检查 effect 是否“脏”(需要重新执行)。
  get dirty(): boolean {
    return isDirty(this)
  }
}

// -----------------------------
// Batching and queue handling
// -----------------------------

// EN: depth of nested batches; batching defers triggers until matching endBatch.
// CN: 批处理深度计数;允许嵌套批处理,只有当最外层 endBatch 被调用时才真正触发队列中的 effects。
let batchDepth = 0
let batchedSub: Subscriber | undefined
let batchedComputed: Subscriber | undefined

// EN: Add a subscriber to the batch queue. Computed effects are queued separately.
// CN: 将 subscriber 加入批处理队列。computed 类型单独队列以保证更新顺序/语义。
export function batch(sub: Subscriber, isComputed = false): void {
  sub.flags |= EffectFlags.NOTIFIED
  if (isComputed) {
    sub.next = batchedComputed
    batchedComputed = sub
    return
  }
  sub.next = batchedSub
  batchedSub = sub
}

/** @internal */
// EN: Start a batch: increase nesting depth.
// CN: 开始一个批处理(深度 +1)。
export function startBatch(): void {
  batchDepth++
}

/** Run batched effects when all batches have ended @internal */
// EN: End batch: if outermost, flush queued computed first (clear notified) then normal effects.
// CN: 结束批处理:若为最外层,则先处理 batchedComputed(仅清理状态),再处理 batchedSub 并触发正常 effects。
export function endBatch(): void {
  if (--batchDepth > 0) {
    return
  }

  if (batchedComputed) {
    let e: Subscriber | undefined = batchedComputed
    batchedComputed = undefined
    while (e) {
      const next: Subscriber | undefined = e.next
      e.next = undefined
      e.flags &= ~EffectFlags.NOTIFIED
      e = next
    }
  }

  let error: unknown
  while (batchedSub) {
    let e: Subscriber | undefined = batchedSub
    batchedSub = undefined
    while (e) {
      const next: Subscriber | undefined = e.next
      e.next = undefined
      e.flags &= ~EffectFlags.NOTIFIED
      if (e.flags & EffectFlags.ACTIVE) {
        try {
          // ACTIVE flag is effect-only
          ;(e as ReactiveEffect).trigger()
        } catch (err) {
          if (!error) error = err
        }
      }
      e = next
    }
  }

  if (error) throw error
}

// -----------------------------
// Dep tracking helpers
// -----------------------------

// EN: prepareDeps marks existing links with version -1 and saves previous activeLink
//     so that after running the effect we can detect which deps were not re-used.
// CN: prepareDeps 将现有依赖链(link)的 version 标为 -1,并保存原先的 activeLink。
//     运行后通过版本判断哪些依赖未被重新使用(用于移除过时订阅)。
function prepareDeps(sub: Subscriber) {
  // Prepare deps for tracking, starting from the head
  for (let link = sub.deps; link; link = link.nextDep) {
    // set all previous deps' (if any) version to -1 so that we can track
    // which ones are unused after the run
    // 将之前的每个 link 的 version 设为 -1,以便在执行后识别未使用的 dep
    link.version = -1
    // store previous active sub if link was being used in another context
    // 保存之前 link.dep 的 activeLink(如果该 link 被其他 subscriber 使用)
    link.prevActiveLink = link.dep.activeLink
    // 将 dep 的 activeLink 指向当前 link,以标识当前 link 为激活状态
    link.dep.activeLink = link
  }
}

// EN: cleanupDeps traverses from tail to head and removes links whose version remained -1
//     (i.e., not used in latest run). Also restores dep.activeLink to previous.
// CN: cleanupDeps 从尾向头遍历;对于仍为 -1 的 link(未在此次运行中使用),从 dep 的订阅列表移除。
//     同时恢复 dep.activeLink 为之前保存的值。
function cleanupDeps(sub: Subscriber) {
  // Cleanup unsued deps
  let head
  let tail = sub.depsTail
  let link = tail
  while (link) {
    const prev = link.prevDep
    if (link.version === -1) {
      if (link === tail) tail = prev
      // unused - remove it from the dep's subscribing effect list
      // 未使用:从 dep 的订阅者列表中移除
      removeSub(link)
      // also remove it from this effect's dep list
      // 也从当前 effect 的 dep 双链表中移除该 link
      removeDep(link)
    } else {
      // The new head is the last node seen which wasn't removed
      // 新的 head 为最后一个未被移除的节点(从尾向前)
      head = link
    }

    // restore previous active link if any
    // 恢复 link.dep 的 activeLink 为保存的 prevActiveLink
    link.dep.activeLink = link.prevActiveLink
    link.prevActiveLink = undefined
    link = prev
  }
  // set the new head & tail
  // 更新 effect 的 deps head / tail 引用
  sub.deps = head
  sub.depsTail = tail
}

// EN: isDirty determines if a subscriber should re-run by checking deps' version or computed status.
// CN: isDirty 检查 subscriber 是否“脏”:遍历其 deps,若 dep.version 与 link.version 不匹配,或者对应的 computed 需要刷新则为脏。
function isDirty(sub: Subscriber): boolean {
  for (let link = sub.deps; link; link = link.nextDep) {
    if (
      link.dep.version !== link.version ||
      (link.dep.computed &&
        (refreshComputed(link.dep.computed) ||
          link.dep.version !== link.version))
    ) {
      return true
    }
  }
  // @ts-expect-error only for backwards compatibility where libs manually set
  // this flag - e.g. Pinia's testing module
  // EN: backward-compat check for libraries that manually set `_dirty`.
  // CN: 向后兼容:某些库(如 Pinia 测试模块)可能手动设置 `_dirty`。
  if (sub._dirty) {
    return true
  }
  return false
}

// -----------------------------
// Computed refresh logic
// -----------------------------

/**
 * Returning false indicates the refresh failed
 * @internal
 */
export function refreshComputed(computed: ComputedRefImpl): undefined {
  // EN: If computed is tracking and not marked dirty, no need to refresh.
  // CN: 若 computed 正在 TRACKING 且未标记为 DIRTY,则无需刷新。
  if (
    computed.flags & EffectFlags.TRACKING &&
    !(computed.flags & EffectFlags.DIRTY)
  ) {
    return
  }
  // EN: clear DIRTY flag since we are about to recompute/validate.
  // CN: 清除 DIRTY 标志(准备刷新或验证)。
  computed.flags &= ~EffectFlags.DIRTY

  // EN: fast path: if no global reactive change (globalVersion unchanged), skip.
  // CN: 全局版本快速路径:若 computed.globalVersion 与全局 globalVersion 相同,说明自上次计算后没有全局 reactive 变更,直接跳过。
  if (computed.globalVersion === globalVersion) {
    return
  }
  computed.globalVersion = globalVersion

  // EN: SSR / no-subscriber / already evaluated handling:
  //    If there's no SSR and computed was evaluated before and either has no deps
  //    or isn't dirty, we can skip re-evaluation.
  // CN: SSR 情况(服务端渲染没有 render effect)、无 deps 或已经评估且不脏时可以跳过重新评估。
  if (
    !computed.isSSR &&
    computed.flags & EffectFlags.EVALUATED &&
    ((!computed.deps && !(computed as any)._dirty) || !isDirty(computed))
  ) {
    return
  }

  // EN: mark as running and set up activeSub/shouldTrack to collect deps while evaluating.
  // CN: 将 computed 标记为 RUNNING,并设置 activeSub、shouldTrack 以便在求值时收集依赖。
  computed.flags |= EffectFlags.RUNNING

  const dep = computed.dep
  const prevSub = activeSub
  const prevShouldTrack = shouldTrack
  activeSub = computed
  shouldTrack = true

  try {
    // EN: evaluate computed.fn with previous value (some computed accept oldValue).
    // CN: 执行 computed.fn,可带入旧值作为参数(实现细节依赖具体 computed 实现)。
    prepareDeps(computed)
    const value = computed.fn(computed._value)
    // EN: if first time (dep.version===0) or value changed, set EVALUATED and update _value.
    // CN: 若是首次或值发生变化,设置 EVALUATED,将新值赋给 _value,并增加 dep.version。
    if (dep.version === 0 || hasChanged(value, computed._value)) {
      computed.flags |= EffectFlags.EVALUATED
      computed._value = value
      dep.version++
    }
  } catch (err) {
    // EN: ensure version increments on error to avoid silent stale caches.
    // CN: 出错时也增加 dep.version,避免缓存停滞导致数据不更新。
    dep.version++
    throw err
  } finally {
    // EN: restore active effect and tracking state, cleanup deps, clear RUNNING flag.
    // CN: 恢复 activeSub 与 shouldTrack,清理 deps,并清除 RUNNING 标志。
    activeSub = prevSub
    shouldTrack = prevShouldTrack
    cleanupDeps(computed)
    computed.flags &= ~EffectFlags.RUNNING
  }
}

// -----------------------------
// Helpers to remove subscriptions / deps
// -----------------------------

// EN: removeSub unlinks a subscription link from both dep and the subscriber.
//     `soft` indicates not to decrement sc or remove entry from dep.map (used when computed unsubscribes soft).
// CN: removeSub 将 link 从 dep 的订阅链表和 subscriber 的链表中解开。
//     soft 参数用于 computed 的“软取消订阅”(不减少 dep.sc,也不从 dep.map 删除),以便 computed 能被 GC。
function removeSub(link: Link, soft = false) {
  const { dep, prevSub, nextSub } = link
  if (prevSub) {
    prevSub.nextSub = nextSub
    link.prevSub = undefined
  }
  if (nextSub) {
    nextSub.prevSub = prevSub
    link.nextSub = undefined
  }
  if (__DEV__ && dep.subsHead === link) {
    // was previous head, point new head to next
    // EN: dev-only: 更新 dep.subsHead(如果需要)。
    // CN: 开发模式:若 link 是旧的 head,更新 head 指向 next。
    dep.subsHead = nextSub
  }

  if (dep.subs === link) {
    // was previous tail, point new tail to prev
    // EN: 更新 dep.subs(tail 指针)。
    // CN: 若 link 是 tail(dep.subs),将 tail 指向 prevSub。
    dep.subs = prevSub

    if (!prevSub && dep.computed) {
      // EN: if computed and no tail after removal, unsubscribe it from all deps so it can be GCed.
      // CN: 如果这是 computed 并且没有 prevSub(意味着 dep 变为空),对 computed 进行彻底“软取消订阅”以便 GC。
      dep.computed.flags &= ~EffectFlags.TRACKING
      for (let l = dep.computed.deps; l; l = l.nextDep) {
        // here we are only "soft" unsubscribing because the computed still keeps
        // referencing the deps and the dep should not decrease its sub count
        // EN: 软取消订阅:不减 dep.sc
        // CN: 软取消订阅(不减少 dep.sc),以保持 computed 仍引用 deps 的结构一致性。
        removeSub(l, true)
      }
    }
  }

  if (!soft && !--dep.sc && dep.map) {
    // #11979
    // property dep no longer has effect subscribers, delete it
    // this mostly is for the case where an object is kept in memory but only a
    // subset of its properties is tracked at one time
    // EN: 如果非 soft,且 subscriber count 减为 0,并且 dep.map 存在,则从 dep.map 删除该键,节省内存。
    // CN: 当某属性再无订阅者时(并且 dep.map 存在),删除该属性的 dep 条目以释放内存。
    dep.map.delete(dep.key)
  }
}

// EN: removeDep unlinks a link from the subscriber's dep double-linked list.
// CN: removeDep 从 effect/computed 的 deps(双向链)中移除 link 链接。
function removeDep(link: Link) {
  const { prevDep, nextDep } = link
  if (prevDep) {
    prevDep.nextDep = nextDep
    link.prevDep = undefined
  }
  if (nextDep) {
    nextDep.prevDep = prevDep
    link.nextDep = undefined
  }
}

// -----------------------------
// Public API: effect / stop
// -----------------------------

export interface ReactiveEffectRunner<T = any> {
  (): T
  effect: ReactiveEffect
}

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
  // EN: If fn is already a runner (i.e., we passed the runner itself), extract its effect.fn
  // CN: 允许把已有 runner 直接传入 effect,若传入的是 runner,则取出其内部的 fn。
  if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // EN: create new ReactiveEffect, extend with options (scheduler, hooks...), run immediately.
  // CN: 创建 ReactiveEffect 实例,扩展 options(合并到实例上),并立即执行一次以收集初始 deps。
  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
}

/**
 * Stops the effect associated with the given runner.
 *
 * @param runner - Association with the effect to stop tracking.
 */
export function stop(runner: ReactiveEffectRunner): void {
  // EN: stop wrapper: call stop on the underlying ReactiveEffect instance.
  // CN: stop 的封装:调用 runner.effect.stop() 停止该 effect。
  runner.effect.stop()
}

// -----------------------------
// Global tracking toggle utilities
// -----------------------------

/** @internal */
export let shouldTrack = true
const trackStack: boolean[] = []

/** Temporarily pauses tracking. */
// EN: pause tracking: push current state and set shouldTrack=false
// CN: 暂时暂停依赖收集:把当前 shouldTrack 压入栈,然后置 false。
export function pauseTracking(): void {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

/** Re-enables effect tracking (if it was paused). */
// EN: enable tracking: push current state and set shouldTrack=true
// CN: 开启依赖收集(也会保存先前状态以便 later reset)。
export function enableTracking(): void {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

/** Resets the previous global effect tracking state. */
// EN: restore previous shouldTrack from stack (or default to true).
// CN: 重置为上一次的 shouldTrack 状态(若无则默认为 true)。
export function resetTracking(): void {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

// -----------------------------
// onEffectCleanup / cleanupEffect
// -----------------------------

/**
 * Registers a cleanup function for the current active effect.
 * The cleanup function is called right before the next effect run, or when the
 * effect is stopped.
 *
 * Throws a warning if there is no current active effect. The warning can be
 * suppressed by passing `true` to the second argument.
 *
 * @param fn - the cleanup function to be registered
 * @param failSilently - if `true`, will not throw warning when called without
 * an active effect.
 */
// EN: Register a cleanup callback associated with the current active ReactiveEffect.
// CN: 为当前活跃的 ReactiveEffect 注册 cleanup 回调(在下一次运行前或 stop 时会执行)。
export function onEffectCleanup(fn: () => void, failSilently = false): void {
  if (activeSub instanceof ReactiveEffect) {
    activeSub.cleanup = fn
  } else if (__DEV__ && !failSilently) {
    warn(
      `onEffectCleanup() was called when there was no active effect` +
        ` to associate with.`,
    )
  }
}

// EN: Execute and clear effect cleanup (run without any active effect).
// CN: 执行 effect 的 cleanup(在无 activeEffect 的上下文中运行),然后清空 cleanup 引用。
function cleanupEffect(e: ReactiveEffect) {
  const { cleanup } = e
  e.cleanup = undefined
  if (cleanup) {
    // run cleanup without active effect
    // EN: run the cleanup with activeSub temporarily unset to avoid accidental tracking.
    // CN: 运行 cleanup 时临时清除 activeSub,防止 cleanup 内部访问响应式数据导致错误的依赖收集。
    const prevSub = activeSub
    activeSub = undefined
    try {
      cleanup()
    } finally {
      activeSub = prevSub
    }
  }
}

Web发展与Vue.js导读

作者 excel
2025年9月27日 05:29

Web,即 World Wide Web,中文译作“万维网”。从静态网页到现代前端框架,Web的发展经历了几个明显阶段。理解这些阶段,有助于更好地理解 Vue 的出现背景和价值。


一、Web的历史演变

1. 石器时代:静态网页

早期网页没有数据库支持,本质上是一张可以在网络上浏览的“报纸”。用户浏览网页时,内容固定、交互有限。直到 CGI 技术的出现,网页才能通过小段代码与数据库或文件系统进行交互,例如 1998 年的 Google 就依赖此技术。

特点:

  • 页面静态,浏览体验单一
  • 数据交互能力有限

2. 文明时代:服务端渲染

2005 年左右,ASP(微软)和 JSP(Java Server Pages)开始流行,取代了 CGI 技术。它们可以在服务器端处理逻辑并生成 HTML 返回客户端。示例 JSP 代码:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<title>JSP demo</title>

特点:

  • 增强了 Web 与服务器交互的安全性
  • 页面灵活度低,每次请求都依赖服务器渲染
  • 同期,Ajax 技术开始普及,实现异步数据更新

3. 工业革命时代:前端框架的出现

移动设备普及后,SPA(Single Page Application 单页面应用)开始兴起。Backbone、EmberJS、AngularJS 等前端框架陆续出现,提高了开发效率,降低了开发门槛。

特点:

  • 提高开发效率,减少重复劳动
  • 解决复杂页面交互问题
  • 初期 SPA 面临 SEO 和复杂 View 绑定问题

4. 百花齐放时代:现代前端

如今,前端技术多样化,框架和工具层出不穷,每种技术都为特定场景提供解决方案。Vue 的出现就是为了解决开发灵活性和易用性问题。


二、Vue.js简介

Vue.js(/vjuː/,简称 Vue)是一个用于构建用户界面的开源 JavaScript 框架,也是创建单页应用的前端框架。它专注于视图层,同时能方便地处理数据更新,实现视图与模型交互。

  • 作者:尤雨溪
  • 发布:2014 年 2 月
  • 特点:轻量、易上手、高开发效率
  • 社区活跃度:GitHub 上星标数排名前三

三、Vue核心特性

1. 数据驱动(MVVM)

MVVM 模型包括:

  • Model:处理业务逻辑和服务器交互
  • View:负责 UI 展示
  • ViewModel:连接 Model 与 View,实现双向绑定

简图:

Model ↔ ViewModel ↔ View

2. 组件化

概念:将界面和逻辑抽象为独立可复用单元,每个 .vue 文件即为一个组件。

优势:

  • 降低耦合度,可快速替换组件
  • 调试方便,定位问题高效
  • 提高可维护性,组件复用提升整体系统质量

3. 指令系统

指令是带 v- 前缀的特殊属性,当数据改变时,DOM 自动响应更新。

常用指令:

  • 条件渲染:v-if
  • 列表渲染:v-for
  • 属性绑定:v-bind
  • 事件绑定:v-on
  • 双向绑定:v-model

与传统开发相比,Vue 不直接操作 DOM,而是通过数据驱动视图变化。


四、Vue与传统开发对比

以“注册账号”功能为例:

  • jQuery 实现

    • 手动获取 DOM 元素
    • 点击按钮显示/隐藏页面元素
    • 逻辑耦合度高
  • Vue 实现

    • 使用变量控制 DOM 显示与否
    • 点击按钮只需修改变量,DOM 自动更新
    • 核心理念:操作数据,而非直接操作 DOM

五、Vue与React对比

特性 Vue React
组件化
服务器端渲染 支持 支持
虚拟 DOM
数据驱动视图 可变数据 不可变数据
原生方案 Weex React Native
构建工具 vue-cli Create React App
组件通信 子向父通过事件和回调 子向父通过回调函数
diff算法 双向指针边对比边更新 diff队列批量更新DOM

Vue 与 React 没有绝对优劣,选择取决于具体场景需求。


参考资料


**声明:**本文部分内容借助 AI 辅助生成,并由作者整理审核。

昨天 — 2025年9月26日首页

楖览:Vue3 源码研究导读

作者 excel
2025年9月26日 20:50

前言

Vue3 的源码采用 模块化 Monorepo 架构,分布在 packages/ 目录下。每个模块都承担着清晰的职责:有的处理模板编译,有的负责运行时渲染,有的提供响应式引擎。
理解这些模块的关系,是研究源码的第一步。


一、编译层

1. compiler-core

  • 作用:模板编译的核心逻辑。

    • 输入:模板字符串 <div>{{ msg }}</div>
    • 输出:渲染函数源码
  • 特点:平台无关,只管 AST 生成和转换。

  • 示例

    import { baseParse } from '@vue/compiler-core'
    
    const ast = baseParse('<p>{{ hello }}</p>')
    console.log(ast.children[0].tag) // 输出 "p"
    

2. compiler-dom

  • 作用:扩展 compiler-core,加入浏览器平台相关逻辑。

  • 应用场景:处理 DOM 专用指令,例如 v-model、事件绑定。

  • 示例

    import { compile } from '@vue/compiler-dom'
    
    const { code } = compile('<button @click="count++">{{ count }}</button>')
    console.log(code) 
    // 输出渲染函数源码字符串,内部包含 _createElementVNode 等调用
    

3. compiler-sfc

  • 作用:处理单文件组件(.vue),解析 <template><script><style>

  • 示例

    import { parse } from '@vue/compiler-sfc'
    
    const source = `
    <template><div>{{ msg }}</div></template>
    <script>export default { data(){ return { msg: 'hi' } } }</script>
    `
    const { descriptor } = parse(source)
    console.log(descriptor.template.content) // "<div>{{ msg }}</div>"
    

4. compiler-ssr

  • 作用:专用于服务端渲染,输出字符串拼接代码而不是 DOM 操作。

  • 示例

    import { compile } from '@vue/compiler-ssr'
    
    const { code } = compile('<div>{{ msg }}</div>')
    console.log(code) 
    // 输出包含 ctx.msg 的字符串拼接函数
    

二、运行时层

1. runtime-core

  • 作用:Vue 运行时的核心,包含组件系统、虚拟 DOM、生命周期调度。

  • 特点:不依赖任何平台 API,可移植。

  • 示例

    import { h, render } from '@vue/runtime-core'
    
    const vnode = h('h1', null, 'Hello Core')
    render(vnode, document.body)
    

2. runtime-dom

  • 作用:为浏览器环境实现 runtime-core 的渲染逻辑,调用真实的 DOM API。

  • 示例

    import { createApp } from 'vue'
    
    const App = {
      data: () => ({ count: 0 }),
      template: `<button @click="count++">{{ count }}</button>`
    }
    
    createApp(App).mount('#app')
    

3. runtime-test

  • 作用:提供一个测试用渲染器,不依赖真实 DOM。

  • 示例

    import { createApp, h } from '@vue/runtime-test'
    
    const App = { render: () => h('div', 'test') }
    const root = {}
    createApp(App).mount(root)
    
    console.log(root.children[0].type) // "div"
    

4. server-renderer

  • 作用:在 Node.js 环境下生成 HTML 字符串,配合 compiler-ssr 使用。

  • 示例

    import { renderToString } from '@vue/server-renderer'
    import { createSSRApp } from 'vue'
    
    const app = createSSRApp({ template: `<h1>Hello SSR</h1>` })
    const html = await renderToString(app)
    console.log(html) // "<h1>Hello SSR</h1>"
    

三、基础模块

1. reactivity

  • 作用:Vue 响应式系统的核心。

  • 示例

    import { ref, effect } from '@vue/reactivity'
    
    const count = ref(0)
    effect(() => console.log('count changed:', count.value))
    
    count.value++ // 触发 effect
    

2. shared

  • 作用:公共工具函数与常量,整个源码都会用到。

  • 示例

    import { isArray } from '@vue/shared'
    
    console.log(isArray([1, 2, 3])) // true
    

四、整合入口

1. vue

  • 作用:开发者使用的入口包,整合了所有子模块。

  • 示例

    import { createApp, ref } from 'vue'
    
    const App = {
      setup() {
        const msg = ref('Hello Vue3')
        return { msg }
      },
      template: `<p>{{ msg }}</p>`
    }
    
    createApp(App).mount('#app')
    

五、模块关系图

        compiler-core
         /       \
 compiler-dom   compiler-ssr
       |             |
   compiler-sfc   server-renderer
       |
     vue (整合入口)
       |
 runtime-core —— reactivity —— shared
       |
 runtime-dom / runtime-test

六、结语

Vue3 的源码像一座“分层工厂”:

  • 编译层:把模板翻译成渲染函数。
  • 运行时层:执行渲染函数,生成 DOM 或字符串。
  • 基础模块:提供响应式与工具函数。
  • 整合入口:最终打包成 vue,交到开发者手里。

这张导览图与示例,能帮助你快速定位源码,带着问题深入研究细节。

本文部分内容借助 AI 辅助生成,并由作者整理审核。

前端项目中的测试分类与实践 —— 以 Vue 项目为例

作者 excel
2025年9月26日 20:42

在现代前端工程化体系中,测试已经成为保障代码质量和开发效率的关键环节。一个大型框架(如 Vue)通常会设计多种测试命令,来覆盖不同层面的需求。以下将对常见的几类测试命令进行拆解说明,并配合示例代码来帮助理解。


一、单元测试(test-unit

概念

单元测试(Unit Test)关注的是最小逻辑单元,例如一个函数或一个小组件。

示例

// math.ts
export function add(a: number, b: number) {
  return a + b;
}
// math.test.ts
import { describe, it, expect } from 'vitest';
import { add } from './math';

describe('math utils', () => {
  it('add should return correct sum', () => {
    expect(add(1, 2)).toBe(3); // ✅ 测试基本逻辑
  });

  it('add should work with negative numbers', () => {
    expect(add(-1, 5)).toBe(4);
  });
});

👉 说明

  • 粒度小,执行快。
  • 不依赖构建产物,直接跑源码。

二、端到端测试(test-e2e

概念

端到端测试(E2E Test)是模拟用户的实际操作来验证系统行为,常用于验证打包后的产物。

示例

<!-- App.vue -->
<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>
// app.e2e.test.ts
import { test, expect } from '@playwright/test';

test('button click should increase counter', async ({ page }) => {
  await page.goto('http://localhost:5173'); // 假设启动了 dev server
  const button = page.getByRole('button');
  await button.click();
  await expect(button).toHaveText('Clicked 1 times'); // ✅ 模拟真实点击
});

👉 说明

  • 需要先 build 出产物,然后在浏览器中运行。
  • 粒度大,接近真实用户体验。
  • 执行速度比单测慢。

三、类型声明测试(test-dts / test-dts-only

概念

类型测试的目标是保证生成的 .d.ts 文件能正常工作,让 TypeScript 用户拥有良好的类型提示。

示例

// vue-shim.d.ts (假设库生成的声明文件)
export function createApp(rootComponent: any): {
  mount(selector: string): void;
};
// dts.test.ts
import { createApp } from 'vue';

// ✅ 正确用法
createApp({}).mount('#app');

// ❌ 错误用法:少了 mount 参数
// 这里应该触发 TS 编译错误
// createApp({}).mount();

👉 说明

  • test-dts 会先生成 .d.ts 文件再检查。
  • test-dts-only 直接用现有的 .d.ts 文件进行编译验证。
  • 关键在于保障 API 类型和实际逻辑一致。

四、覆盖率测试(test-coverage

概念

覆盖率测试不仅运行单元测试,还会统计代码哪些部分被执行,输出报告(如语句、分支、函数、行覆盖率)。

示例

// stringUtils.ts
export function greet(name?: string) {
  if (name) {
    return `Hello, ${name}`;
  }
  return 'Hello, guest';
}
// stringUtils.test.ts
import { describe, it, expect } from 'vitest';
import { greet } from './stringUtils';

describe('greet', () => {
  it('should greet with name', () => {
    expect(greet('Alice')).toBe('Hello, Alice');
  });
});

👉 说明

  • 测试只覆盖了有 name 的情况,guest 分支没测到。
  • test-coverage 运行后会提示覆盖率不足,提醒你写额外测试:
it('should greet guest when no name provided', () => {
  expect(greet()).toBe('Hello, guest');
});

五、对比总结

命令 测试范围 示例场景
test-unit 模块逻辑 add(1,2) → 3
test-e2e 打包产物 & 用户行为 点击按钮计数器增加
test-dts 类型声明生成 + 检查 createApp().mount() 类型是否报错
test-dts-only 仅检查现有类型声明 不构建,直接验证
test-coverage 单测 + 覆盖率报告 提示 guest 分支未覆盖
test 全部测试集合 本地一键跑完

六、潜在问题

  1. E2E 测试执行慢:CI/CD 环境可能成为瓶颈。
  2. 覆盖率追求过度:高覆盖率不代表高质量,测试内容比数字更重要。
  3. 类型声明忽视:很多库项目容易忽略 d.ts 测试,导致 TS 用户踩坑。
  4. 依赖构建链路:像 test-e2etest-dts 一旦构建失败,测试链全挂。

结语

不同的测试类型各有侧重:

  • 单元测试 → 保证基础逻辑正确。
  • 端到端测试 → 模拟用户真实场景。
  • 类型测试 → 保证 TypeScript 用户的体验。
  • 覆盖率测试 → 衡量测试充分性。

它们共同构建了一个完整的质量保障体系,帮助项目在开发和交付中保持高可靠性。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

为什么要使用 TypeScript:TS 相比 JavaScript 的优势

作者 excel
2025年9月25日 21:30

一、概念

JavaScript(JS)是动态类型语言,灵活但不安全;TypeScript(TS)是 JS 的超集,提供 静态类型检查智能提示更好的协作性,最终编译为 JS 在任何环境中运行。

一句话:TS 让 JS 更安全、更高效,尤其在多人协作与调用复杂 API 时优势明显。


二、原理

  1. 静态类型系统 —— 提前捕获错误。
  2. 类型声明文件(.d.ts —— 为 API、库提供类型说明,让编辑器能直接提示返回值与参数类型。
  3. 智能提示 (IntelliSense) —— 基于类型定义,IDE 能自动补全、展示 API 返回值结构。

三、对比(TS vs JS)

特性 JavaScript TypeScript
API 调用 只能查文档或运行时打印 console.log IDE 自动提示参数和返回值类型
错误发现 多数在运行时发现 大部分在编译期发现
可维护性 依赖开发者自觉 接口、类型明确,重构安全
开发效率 API 使用需频繁切换到文档 API 类型直接可见,减少错误调用

四、实践

例子 1:调用 API 时返回值结构不明确

JavaScript

fetch("/api/user")
  .then(res => res.json())
  .then(data => {
    console.log(data.id); // 运行时报错:如果 data 里没有 id
  });
  • JS 下,data 的结构完全不明确,开发者只能 console.log 去猜。

TypeScript

interface User {
  id: number;
  name: string;
}

fetch("/api/user")
  .then(res => res.json())
  .then((data: User) => {
    console.log(data.id); // ✅ 提示为 number 类型
    console.log(data.age); 
    // ❌ 编译时报错:Property 'age' does not exist on type 'User'
  });
  • TS 自动告诉你 data 里有哪些字段,减少 API 使用错误。

例子 2:调用第三方库 API

JavaScript

import _ from "lodash";

_.flattenDeep([1, [2, [3, [4]]]]); 
// 开发者必须查文档才能知道 flattenDeep 的返回类型

TypeScript

import _ from "lodash";

const arr = _.flattenDeep([1, [2, [3, [4]]]]);
// IDE 提示 arr: any[] → 立即知道返回值是数组
  • TS 的 .d.ts 类型声明让 IDE 显示 API 的参数与返回值,减少文档查找时间。

五、拓展

  1. 快速掌握陌生库

    • 使用 TS 时,导入一个新库后,光是把鼠标悬停在函数上就能看到参数与返回类型 → 等于内置文档
    • 这在 React、Vue、Node.js 的第三方库开发时非常高效。
  2. 团队协作中的 API 约定

    • 后端接口返回类型定义在 TS 的 interfacetype 中,前端团队成员直接用 → 避免反复询问字段含义。
  3. 与现代框架契合

    • Vue3、React 都内置对 TS 的友好支持。
    • 比如在 React 中,propsstate 有了明确类型后,组件使用错误能立刻发现。

六、潜在问题

  1. 学习成本 —— 类型系统初学者需要适应。
  2. 编译开销 —— 多一步转译,但现在工具链(Vite、Webpack、tsc)优化后影响不大。
  3. 第三方库类型缺失 —— 部分库没有类型定义文件时需要安装 @types/xxx 或手写声明。

总结

TypeScript 相比 JavaScript 最大的优势不仅在于 静态类型安全,更在于 提升开发效率

  • 调用 API 时自动提示参数与返回值,无需频繁查文档;
  • 减少逻辑错误与拼写错误;
  • 在多人协作、大型项目中保证一致性和可维护性。

因此,TS 已成为现代前端工程化开发的主流选择。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

昨天以前首页

基于两台服务器的蓝绿切换实现零下线更新

作者 excel
2025年9月25日 08:36

一、概念

在网站或应用的运维中,“零下线更新”是指在发布新版本时,用户访问不受影响,不会出现中断、502 错误或服务不可达。常见的一种做法是 蓝绿部署(Blue-Green Deployment)

  • 蓝环境(Blue) :当前运行的生产版本,正在处理用户请求;
  • 绿环境(Green) :新版本环境,在后台完成更新与测试;
  • 切换:当绿环境稳定后,将流量从蓝环境切换到绿环境;如果发现问题,再快速切回蓝环境。

二、原理

  1. 两台服务器独立运行

    • 一台服务器作为现网服务;
    • 另一台空闲服务器用于新版本部署。
  2. Nginx 负载均衡控制流量

    • 在前端通过 upstream 管理两台服务器;
    • 切换时修改权重或启用/禁用某台服务器,reload Nginx 配置即可生效。
  3. 平滑切换机制

    • Nginx reload 是平滑的,不会直接杀掉旧进程;
    • 旧的 Worker 会等待当前请求处理完毕后再退出;
    • 新 Worker 会立即接管新请求,从而实现零停机。

三、对比

  • 单机热更新:通过软链切换或直接覆盖文件来更新,简单但风险高,一旦更新失败会影响整个服务。
  • 蓝绿部署:两台服务器互为备份,流量可随时切换,安全性更高,更新过程对用户透明。
  • 灰度发布:在蓝绿基础上进一步扩展,可逐步分配流量,验证稳定性后再全量切换。

四、实践

Nginx 配置示例

upstream backend {
    server 192.168.1.101:8080 weight=1;  # 蓝环境
    # server 192.168.1.102:8080 weight=0;  # 绿环境(未启用)
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend;
    }
}

切换流程

  1. 在蓝环境正常运行时,更新绿环境;

  2. 完成新版本部署和测试;

  3. 修改 upstream,把流量切到绿环境:

    upstream backend {
        # server 192.168.1.101:8080 weight=0;
        server 192.168.1.102:8080 weight=1;
    }
    
  4. nginx -t && nginx -s reload 平滑生效;

  5. 观察运行情况,如果有问题,立刻切回蓝环境。


五、拓展

  • 灰度流量分配:先给绿环境分配少量流量(如 10%),逐步放量;
  • 健康检查:在 Nginx 或上层负载均衡中增加健康检查,确保异常节点不接收请求;
  • 自动化部署:结合 CI/CD 工具,实现部署、切换、回滚一体化。

六、潜在问题与不足

  1. 成本问题

    • 至少需要两台服务器,资源利用率偏低;
    • 小型团队可能难以承担额外硬件开销。
  2. 会话保持问题

    • 如果应用依赖本地 Session,切换时可能导致用户登录状态丢失;
    • 通常需要外部存储(Redis、数据库)来做会话共享。
  3. 数据库版本兼容问题

    • 蓝绿服务器共用同一个数据库时,新旧版本可能对数据结构有不同要求;
    • 需要数据库向下兼容,或者提前做 schema 升级。
  4. 切换瞬间的请求不一致

    • 如果有长连接(WebSocket/HTTP2),可能部分连接仍然留在旧环境;
    • 需要在应用层设计容错机制。
  5. DNS 切换滞后(如果用 DNS 方式)

    • DNS 缓存会导致部分用户仍然访问旧环境,影响一致性。

七、总结

两台服务器的蓝绿切换方案,是实现 零下线更新 的经典方式,简单、可靠、回滚迅速。它特别适合对 服务可用性要求高 的业务场景。但在资源成本、会话保持、数据库兼容和长连接管理上仍有不足,需要结合 共享存储、灰度发布、自动化工具 才能做到真正的高可用与稳定。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

❌
❌