普通视图

发现新文章,点击刷新页面。
今天 — 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月27日 00:58

作为一名拥有四年前端开发经验的工程师,我主要在互联网 C 端项目工作,常常需要处理金额计算、数据加密、表单校验、时间处理、状态持久化等问题。 在这类项目中,选对工具能大大提升开发效率、降低 bug 率。

本文整理了我在实际项目中长期使用、验证过的高频实用库,涵盖数值处理、加密、校验、时间管理、状态持久化、XML 解析以及代码辅助开发工具。

希望这份清单能帮助你在项目中少踩坑、快开发!


1. 数字精度处理:decimal.js

核心问题:
JavaScript 原生浮点数计算会产生精度问题,例如:

console.log(0.1 + 0.2); // 0.30000000000000004

在涉及金额或积分计算时,这种误差可能造成严重问题。

为什么选择 decimal.js

  • 支持任意精度,不会出现浮点数误差。
  • API 简单易用,比 big.js 功能更完整。

示例 1:基础计算

import Decimal from 'decimal.js';

// 金额计算
const total = new Decimal(0.1).plus(0.2).toString();
console.log(total); // "0.3"

示例 2:金融场景 - 保留两位小数

const price = new Decimal(19.995);
console.log(price.toFixed(2)); // "20.00"

示例 3:大数字计算

const largeNumber = new Decimal('123456789123456789').mul(2);
console.log(largeNumber.toString()); // "246913578246913578"

适用场景:

  • 金融项目、支付、订单金额计算
  • 大数字统计(积分、虚拟币等)

2. 数据加密与哈希:crypto-js

核心问题:
前端经常需要做数据安全处理,比如密码加密接口签名Token 加密等。

为什么选择 crypto-js

  • 内置常用加密算法(MD5、SHA1、SHA256、AES、HmacSHA256 等)。
  • 前后端通用,方便与后端保持一致。
  • 体积小,易集成。

示例 1:MD5 加密

import CryptoJS from 'crypto-js';

const password = 'mySecretPassword';
const hash = CryptoJS.MD5(password).toString();
console.log(hash); // f857606c76b9d72353257dbd273c9b9e

示例 2:SHA256 签名

const data = 'userId=123&timestamp=1695712312';
const signature = CryptoJS.SHA256(data).toString();
console.log(signature);

示例 3:AES 加解密

const secretKey = 'abcdef123456';
const encrypted = CryptoJS.AES.encrypt('Sensitive Data', secretKey).toString();
console.log(encrypted);

const decrypted = CryptoJS.AES.decrypt(encrypted, secretKey).toString(CryptoJS.enc.Utf8);
console.log(decrypted); // "Sensitive Data"

适用场景:

  • 登录密码加密
  • 接口安全签名
  • 敏感数据存储(如手机号、身份证号)

3. 表单与数据校验:validator

核心问题:
表单提交前,需要验证邮箱、手机号、URL、身份证号等是否合规。

为什么选择 validator

  • 超过 130 个校验函数,几乎涵盖所有常见场景。
  • 链式调用简单,覆盖率高。

示例 1:校验邮箱

import validator from 'validator';

console.log(validator.isEmail('test@example.com')); // true
console.log(validator.isEmail('not-an-email')); // false

示例 2:校验手机号

// 第二个参数指定地区
console.log(validator.isMobilePhone('13800000000', 'zh-CN')); // true
console.log(validator.isMobilePhone('12345', 'zh-CN')); // false

示例 3:复杂规则组合

function validatePassword(password) {
  return (
    validator.isLength(password, { min: 8 }) &&
    validator.isAlphanumeric(password) &&
    validator.matches(password, /[A-Z]/)
  );
}

console.log(validatePassword('Test1234')); // true

适用场景:

  • 表单验证
  • 数据格式检查(手机号、URL、IP 等)

4. 日期时间处理:moment.js

核心问题:
处理日期和时间,比如格式化、计算时间差、处理时区等。

为什么选择 moment.js

  • 功能强大,API 设计简单。

示例 1:格式化日期

import moment from 'moment';

console.log(moment().format('YYYY-MM-DD HH:mm:ss')); // 2025-09-27 00:55:01

示例 2:计算时间差

const start = moment('2025-09-01');
const end = moment('2025-09-26');

console.log(end.diff(start, 'days')); // 25

示例 3:显示相对时间

console.log(moment('2025-09-25').fromNow()); // "一天前"

适用场景:

  • 订单时间、出行时间、飞机起飞到达时间等
  • 日历、排班等复杂业务

5. 状态持久化:pinia-plugin-persistedstate

核心问题:
使用 Vue + Pinia 时,刷新页面会导致状态丢失。

为什么选择它

  • 无需自己手动写 localStorage/sessionStorage 逻辑。
  • 可配置存储位置、加密策略。

示例:

import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {},
  }),
  persist: {
    storage: localStorage, // 默认 localStorage
  },
});

适用场景:

  • 登录状态保存
  • 表单缓存
  • 用户偏好设置

6. XML 转 JSON:x2js

核心问题:
在旅游、电商、酒店预订等行业,后端可能返回 XML 数据,前端需要解析成 JSON。

为什么选择 x2js

  • 轻量、易用。
  • 比 DOMParser 更直观。

示例:

import X2JS from 'x2js';

const x2js = new X2JS();
const xmlData = `<root><hotel><name>Hilton</name></hotel></root>`;
const jsonObj = x2js.xml2js(xmlData);

console.log(jsonObj.root.hotel.name); // Hilton

适用场景:

  • 旅游、酒店预订、机票系统静态的详情描述
  • 解析第三方 XML 接口

7. 元素定位代码:code-inspector-plugin

核心问题:
当页面出现问题时,想快速定位前端代码位置,但大型项目文件多,手动查找非常耗时。

如果是vue开发者,其实还有一个vue devtools,只能定位到组件级,没法定位到元素的具体行,这儿就不展开说了,因为它的功能不只是定位元素,更多的是调试响应式数据的,Respect!

为什么选择 code-inspector-plugin

  • 点击页面元素,直接跳转到对应的代码文件行(IDE 自动打开)。
  • 特别适合多人协作和大型项目调试。
  • 同事用了都叫好, 还说我怎么不早点分享

示例:Vite 配置 (注意只在开发环境配置)

// vite.config.js
import { defineConfig } from 'vite';
import { codeInspectorPlugin } from 'code-inspector-plugin';

export default defineConfig({
  plugins: [
      codeInspectorPlugin({
          bundler: 'vite',
          showSwitch: true, //开启后会出来一个按钮,点击按钮之后点击想看的元素就能定位了
        }),
  ],
});

使用方式:

  1. 启动项目后,按住 Ctrl + 鼠标左键 点击页面元素。
  2. VSCode 会直接打开对应组件文件。

适用场景:

  • 大型项目调试
  • 新人快速熟悉项目
  • 组件层级深

8. 总结与建议

以上是我在 C 端项目中高频使用的工具库,它们在解决实际问题上都非常高效、稳定。 当然除了这些,还有i18n以及lodash等等,这里就不一一叙述了。

选库建议:

  1. 以业务需求为导向:先明确问题,再选工具。
  2. 关注社区活跃度:定期维护的库更靠谱。
  3. 团队统一标准:减少重复选择,避免兼容性问题。
库名 主要用途 体积 特点
decimal.js 数字精度计算 任意精度,金融常用
crypto-js 加密/哈希 前后端通用,支持多种算法
validator 数据校验 场景覆盖全,表单友好
moment.js / dayjs 日期时间处理 大/小 moment 功能全,dayjs 轻量
pinia-plugin-persistedstate 状态持久化 与 Pinia 完美集成
x2js XML 转 JSON 旅游、酒店行业常用
code-inspector-plugin 代码定位辅助 开发效率提升神器

结语

选对工具不仅能提高开发效率,还能让项目更加稳定、可维护。
希望这篇文章能为你提供一些参考,让你在前端开发的日常工作中更加得心应手。 如果你有其他高频使用的工具库,也欢迎在评论区分享,一起交流!

实现AI对话光标跟随效果

作者 gnip
2025年9月27日 00:29

概述

在使用一些AI对话工具的时候,比如gtp的聊天,在内容不断生成过程中,末尾会有光标跟随的特效,标识当前的实时位置,下面我们自己模拟实现一下。

效果

动画.gif

实现思路

  • 首先聊天内容是实时不断更新的过程,实现通过模拟数据生成
  • 要实现跟随文本生成最后位置生成一个圆点(自定义),需要找到最后一个文本节点
  • 然后追加一个文本
  • 获取文本相对页面的位置信息
  • 设置光标dom元素到上面的位置
  • 最后删除多余的文本

涉及到的DOM API

如下两个API在我们获取位置的时候非常关键,可以自行查阅相关用法

  • getBoundingClientRect
  • document.createRange

实现

生成聊天内容

使用如下测试数据

 const str = `核心对比:现代公共性观赏 vs 古代私人雅集式观赏。 
        开头引用民间说法和《本草纲目》,指出大蒜对眼睛有害。
        接着从临床经验、现代医学、中医理论多角度解释为什么有害。
        然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        作者态度是:
        现代公共观赏是主流,若强装古人雅集式观赏会被嘲笑。
        选项分析:
        历史越往后发展,艺术品越具有公共性——文段
        中国人艺术修为在不断进化——文段没有谈修为进化然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        我们分析一下文段结构:
        开头引用民间说法和《本草纲目》,指出大蒜对眼睛有害。然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        接着从临床经验、现代医学、中医理论多角度解释为什么有害。
       然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        `
        
           function transformTag(str) {
            return str.split("\n").map(t => `<p>${t}</p>`).join("")
        }

延迟函数

一个简单的Promise应用

   function delay(duration) {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve();
                }, duration);
            });
        }

获取最后一文本节点

      function getLastTextNode(node) {
            if (node.nodeType === Node.TEXT_NODE) {
                return node
            }
            const childNodeList = Array.from(node.childNodes)
            for (let i = childNodeList.length - 1; i >= 0; i--) {
                const child = childNodeList[i]
                const res = getLastTextNode(child)
                if (res) {
                    return res
                }
            }
            return null
        }

更新光标位置

    function updateCursor() {
            const lastTextNode = getLastTextNode(wrapper)
            const curSorNode = document.createTextNode("|")
            if (lastTextNode) {
                lastTextNode.after(curSorNode)
            } else {
                wrapper.appendChild(curSorNode)
            }
            // 获取光标位置元素节点位置

            const range = document.createRange();
            range.setStart(curSorNode, 0);
            range.setEnd(curSorNode, 0);
            const rect = range.getBoundingClientRect();
            const wrapperRect = wrapper.getBoundingClientRect()

            const left = rect.left - wrapperRect.left
            const top = rect.top - wrapperRect.top
            console.log("rect", rect)
            // 设置光标位置
            if (!dot) {
                dot = document.createElement("span")
                dot.className = "blinking-dot"
                document.body.appendChild(dot)
            }
            const dotRect = dot.getBoundingClientRect()
            dot.style.left = rect.left + "px"
            dot.style.top = rect.top + rect.height / 2 - dotRect.height / 2 + "px"
            curSorNode.remove()

        }

渲染开始

 async function renderContent() {
            for (let i = 0; i < str.length; i++) {
                const text = str.slice(0, i);
                const html = transformTag(text)
                wrapper.innerHTML = html
                updateCursor()
                await delay(180)

            }
        }
        renderContent()

样式

    .blinking-dot {
            width: 15px;
            height: 15px;
            background-color: #000;
            /* 圆点颜色 */
            border-radius: 50%;
            position: fixed;
            /* 圆形 */
            animation: blink 0.8s infinite;
            /* 动画设置 */
            box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
            /* 可选的光晕效果 */
        }

        /* 闪烁动画定义 */
        @keyframes blink {
            0% {
                opacity: 1;
                /* 完全显示 */
                transform: scale(1);
                /* 正常大小 */
            }

            50% {
                opacity: 0.3;
                /* 半透明 */
                transform: scale(0.8);
                /* 稍微缩小 */
            }

            100% {
                opacity: 1;
                /* 恢复完全显示 */
                transform: scale(1);
                /* 恢复大小 */
            }
        }

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .blinking-dot {
            width: 15px;
            height: 15px;
            background-color: #000;
            /* 圆点颜色 */
            border-radius: 50%;
            position: fixed;
            /* 圆形 */
            animation: blink 0.8s infinite;
            /* 动画设置 */
            box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
            /* 可选的光晕效果 */
        }

        /* 闪烁动画定义 */
        @keyframes blink {
            0% {
                opacity: 1;
                /* 完全显示 */
                transform: scale(1);
                /* 正常大小 */
            }

            50% {
                opacity: 0.3;
                /* 半透明 */
                transform: scale(0.8);
                /* 稍微缩小 */
            }

            100% {
                opacity: 1;
                /* 恢复完全显示 */
                transform: scale(1);
                /* 恢复大小 */
            }
        }
    </style>
</head>

<body>
    <!-- 内容容器 -->
    <div class="wrapper"></div>
    
    <script>
        // 要显示的文本内容
        const str = `核心对比:现代公共性观赏 vs 古代私人雅集式观赏。 
        开头引用民间说法和《本草纲目》,指出大蒜对眼睛有害。
        接着从临床经验、现代医学、中医理论多角度解释为什么有害。
        然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        作者态度是:
        现代公共观赏是主流,若强装古人雅集式观赏会被嘲笑。
        选项分析:
        历史越往后发展,艺术品越具有公共性——文段
        中国人艺术修为在不断进化——文段没有谈修为进化然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        我们分析一下文段结构:
        开头引用民间说法和《本草纲目》,指出大蒜对眼睛有害。然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        接着从临床经验、现代医学、中医理论多角度解释为什么有害。
        然后不仅讲对眼睛的害处,还提到蒜是发物,会刺激加重其他疾病(如肠炎)。
        `;
        
        // 获取内容容器元素
        const wrapper = document.querySelector('.wrapper');
        // 用于存储闪烁圆点的引用
        let dot = null;
        
        /**
         * 延迟函数,返回一个Promise,在指定时间后resolve
         * @param {number} duration 延迟时间(毫秒)
         * @returns {Promise} 延迟完成的Promise
         */
        function delay(duration) {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve();
                }, duration);
            });
        }
        
        /**
         * 将文本转换为HTML段落
         * @param {string} str 要转换的文本
         * @returns {string} 转换后的HTML字符串
         */
        function transformTag(str) {
            // 按换行符分割文本,每行用<p>标签包裹
            return str.split("\n").map(t => `<p>${t}</p>`).join("");
        }
        
        /**
         * 异步渲染内容,实现打字机效果
         */
        async function renderContent() {
            // 逐个字符显示文本
            for (let i = 0; i < str.length; i++) {
                // 获取当前要显示的文本部分
                const text = str.slice(0, i);
                // 转换为HTML格式
                const html = transformTag(text);
                // 更新容器内容
                wrapper.innerHTML = html;
                // 更新光标位置
                updateCursor();
                // 延迟一段时间,控制打字速度
                await delay(180);
            }
        }
        
        /**
         * 递归查找DOM节点中的最后一个文本节点
         * @param {Node} node 要查找的节点
         * @returns {Node|null} 找到的文本节点或null
         */
        function getLastTextNode(node) {
            // 如果当前节点是文本节点,直接返回
            if (node.nodeType === Node.TEXT_NODE) {
                return node;
            }
            
            // 获取所有子节点并转换为数组
            const childNodeList = Array.from(node.childNodes);
            
            // 从后往前遍历子节点
            for (let i = childNodeList.length - 1; i >= 0; i--) {
                const child = childNodeList[i];
                // 递归查找子节点中的最后一个文本节点
                const res = getLastTextNode(child);
                if (res) {
                    return res;
                }
            }
            
            // 如果没有找到文本节点,返回null
            return null;
        }
        
        /**
         * 更新光标位置
         */
        function updateCursor() {
            // 查找最后一个文本节点
            const lastTextNode = getLastTextNode(wrapper);
            // 创建光标节点(竖线符号)
            const curSorNode = document.createTextNode("|");
            
            // 如果找到文本节点,将光标插入其后
            if (lastTextNode) {
                lastTextNode.after(curSorNode);
            } else {
                // 如果没有文本节点,将光标添加到容器末尾
                wrapper.appendChild(curSorNode);
            }
            
            // 创建Range对象用于获取光标位置
            const range = document.createRange();
            range.setStart(curSorNode, 0); // 设置Range起点
            range.setEnd(curSorNode, 0);   // 设置Range终点
            // 获取光标位置信息
            const rect = range.getBoundingClientRect();
            // 获取容器位置信息
            const wrapperRect = wrapper.getBoundingClientRect();
            
            // 计算相对于容器的位置
            const left = rect.left - wrapperRect.left;
            const top = rect.top - wrapperRect.top;
            console.log("光标位置:", rect);
            
            // 创建或更新闪烁圆点
            if (!dot) {
                // 如果圆点不存在,创建新元素
                dot = document.createElement("span");
                dot.className = "blinking-dot";
                document.body.appendChild(dot);
            }
            
            // 获取圆点尺寸
            const dotRect = dot.getBoundingClientRect();
            // 设置圆点位置:水平位置与光标对齐,垂直位置与光标中心对齐
            dot.style.left = rect.left + "px";
            dot.style.top = rect.top + rect.height / 2 - dotRect.height / 2 + "px";
            
            // 移除临时光标节点
            curSorNode.remove();
        }
        
        // 页面加载完成后开始渲染内容
        window.addEventListener('DOMContentLoaded', () => {
            renderContent();
        });
    </script>
</body>
</html>
昨天 — 2025年9月26日首页

【TS 设计模式完全指南】构建你的专属“通知中心”:深入观察者模式

作者 烛阴
2025年9月26日 22:55

一、什么是观察者模式?

观察者模式(Observer Pattern)是一种行为设计模式,它定义了一种一对多的依赖关系。当一个对象(被称为主题 Subject发布者 Publisher)的状态发生改变时,所有依赖于它的对象(被称为观察者 Observers订阅者 Subscribers)都会得到通知并自动更新。

二、观察者模式的核心组件

  1. 主题接口 (Subject Interface):声明了用于管理观察者的方法,通常包括 subscribe() , unsubscribe() , 和 notify()
  2. 观察者接口 (Observer Interface):声明了所有具体观察者必须实现的通知方法,通常是 update()
  3. 具体主题 (Concrete Subject):实现了主题接口。它维护着一个观察者列表,并在自身状态改变时,调用 notify() 方法通知所有观察者。
  4. 具体观察者 (Concrete Observer):实现了观察者接口。在 update() 方法中定义了收到通知后要执行的具体逻辑。

三、示例:实现一个商品到货通知系统

3.1 定义接口 (Interfaces)

首先定义好我们的“合同”——SubjectObserver 接口。

// Observer Interface
interface IObserver {
    update(subject: ISubject): void;
}

// Subject Interface
interface ISubject {
    subscribe(observer: IObserver): void;
    unsubscribe(observer: IObserver): void;
    notify(): void;
}
  • update 方法传入 subject 本身,这样观察者就可以在需要时从中获取更新后的状态。

3.2 创建具体主题 (Concrete Subject)

我们的 Product 类就是具体主题。它管理自己的库存状态和订阅者列表。

// Concrete Subject
class Product implements ISubject {
    public readonly name: string;
    private observers: IObserver[] = [];
    private _inStock: boolean = false;

    constructor(name: string) {
        this.name = name;
    }

    get inStock(): boolean {
        return this._inStock;
    }

    // 状态改变的方法
    public setStockStatus(status: boolean): void {
        console.log(`\n[PRODUCT]:商品 "${this.name}" 的库存状态变为 ${status ? '有货' : '缺货'}.`);
        this._inStock = status;
        this.notify(); // 状态改变,通知所有观察者!
    }

    public subscribe(observer: IObserver): void {
        const isExist = this.observers.includes(observer);
        if (isExist) {
            return console.log('Observer has been attached already.');
        }
        this.observers.push(observer);
        console.log(`[SYSTEM]: ${observer.constructor.name} 成功订阅 "${this.name}".`);
    }

    public unsubscribe(observer: IObserver): void {
        const observerIndex = this.observers.indexOf(observer);
        if (observerIndex === -1) {
            return console.log('Nonexistent observer.');
        }
        this.observers.splice(observerIndex, 1);
        console.log(`[SYSTEM]: ${observer.constructor.name} 已取消订阅.`);
    }

    public notify(): void {
        console.log(`[PRODUCT]: 正在通知所有 ${this.observers.length} 位订阅者...`);
        for (const observer of this.observers) {
            observer.update(this);
        }
    }
}

3.3 创建具体观察者 (Concrete Observers)

现在,我们创建UI更新器和邮件服务,它们都是观察者。

// Concrete Observer 1: UI Updater
class UINotifier implements IObserver {
    update(subject: ISubject): void {
        if (subject instanceof Product) {
            console.log(
                `[UI]: 收到通知!商品 "${subject.name}" 现在 ${
                    subject.inStock ? '有货' : '缺货'
                },正在更新页面显示...`
            );
        }
    }
}

// Concrete Observer 2: Email Service
class EmailNotifier implements IObserver {
    update(subject: ISubject): void {
        if (subject instanceof Product && subject.inStock) {
            console.log(`[Email]: 收到通知!商品 "${subject.name}" 已到货,正在准备发送邮件...`);
        }
    }
}

3.4 客户端代码:将一切联系起来

const ps5 = new Product('PlayStation 5');

const ui = new UINotifier();
const emailService = new EmailNotifier();

// 订阅
ps5.subscribe(ui);
ps5.subscribe(emailService);

// 状态变化 -> 缺货 (假设初始为缺货,这里为了演示,手动设置一次)
ps5.setStockStatus(false); 
// 此时只会触发UI更新缺货状态,邮件服务因为逻辑判断不会发送邮件

// 关键时刻:到货了!
ps5.setStockStatus(true);
// 此时UI和Email服务都会收到通知并执行相应操作

// 一个用户不再关心了,取消订阅邮件
ps5.unsubscribe(emailService);

// 再次变为缺货
ps5.setStockStatus(false);
// 这次只有UI会收到通知

为了方便大家学习和实践,本文的所有示例代码和完整项目结构都已整理上传至我的 GitHub 仓库。欢迎大家克隆、研究、提出 Issue,共同进步!

📂 核心代码与完整示例: GoF

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

168 元一年的「小红卡」,是小红书打破本地生活红海的钥匙

2025年9月26日 22:45

本地生活领域,小红书突然发力。

2025 年的秋天,本地生活赛道的战火重燃。当京东、阿里重金布局,高德以「扫街榜」切入腹地,整个赛道似乎又回到了「流量、低价、补贴」的经典叙事中。然而,小红书却选择了一条截然不同的路径。

伴随着第三届「马路生活节」在上海、杭州、广州三城的同步开启,小红书正式推出了一款名为「小红卡」的本地生活订阅会员服务。用户花费 168 元购买年卡后,不仅可以获得参与「马路生活节」系列活动的门票,还能在接下来的一年里,于数千家精选本地门店中享受全年全店 9 折的优惠。

小红卡丨来自:小红书

这并非又一个团购套餐的变种,而是在当下本地生活领域极为罕见的平台级付费订阅服务。这张小小的「小红卡」背后,是小红书试图以其独特的社区基因,为这场混战提供一个新解法。这不仅是一次商业模式的探索,更是对小红书能否将线上强大的「种草」势能,高效、顺滑地转化为线下交易闭环的一次关键测试。

「小红卡」是什么?一个「精选」出来的生活方式入口

要理解小红书在本地生活领域的图谋,就必须先读懂「小红卡」这款产品。它并非心血来潮,而是小红书社区逻辑的必然延伸。

「小红书做本地生活是不得不做的事,因为小红书就是一本关于生活方式的书,而生活方式的核心就是吃喝玩乐。」小红书交易产品负责人、本地产品负责人莱昂在接受采访时坦言。这种「不得不做」的背后,是社区用户海量的真实需求。每天,数以亿计的用户在小红书上发现城市的新锐咖啡馆,寻找周末的展览,或是为了一口地道小吃「Mark」下一整条 Citywalk 路线。

然而,从「Mark」到最终完成线下交易,中间始终存在一道鸿沟。过去两年,「马路生活节」作为小红书最大的线下 IP,已成功验证其对线下场景的强大号召力。但莱昂和他的团队也在思考一个更深层的问题:「除了给商家带来人气,我们还能不能直接带来交易?」

小红卡门店丨来自:小红书

「小红卡」正是这个问题的答案。从今年 6 月规划到之后两个月里与商家密集沟通,这款产品的核心逻辑从一而终——精选

与传统平台追求「大而全」的覆盖率不同,「小红卡」从诞生之初就刻上了「小红书精选」的烙印。「我们想把那些真正的好店挑出来,打包给用户一些好的权益,为他们创造一个更轻松的交易环境。」莱昂解释道。这种精选并非基于单一算法或竞价排名,而是深度结合了社区用户的真实口碑,通过 AI 等技术分析与人工探访相结合,筛选出首批上千家门店。

这些门店被打上「小红卡精选」的标识,在 App 内被高亮展示。对用户而言,这意味着交易决策成本的降低。「小红卡」试图传递一个明确信号:这上面的店,经过社区品味验证,值得信赖。而「全店通用 9 折」的权益设计,则进一步简化了交易流程。「我们希望这件事变得简单,用户不用再去费心计算复杂的优惠券。」

对于商家而言,接受「小红卡」的 9 折优惠,所看重的远不止是订单转化,更是其背后高质量的用户群体和独特的口碑效应。这些商家主理人普遍认同,小红书的用户不仅具备消费力,更是生活方式的积极分享者。一笔通过「小红卡」完成的交易,往往能催生一篇高质量的真实笔记,进而吸引更多同频用户。这种「交易换口碑,口碑换流量」的良性循环,是传统流量采买模式无法比拟的。

因此,「小红卡」的本质,是一个由社区共识筛选出的「好店联盟」。它以「马路生活节」为引爆点,试图为用户构建一个从线上种草,到线下一卡通行、轻松完成交易,再回到线上分享体验的完整闭环。

差异化竞争:当「体验」优先于「价格」

将「小红卡」置于整个本地生活赛道的宏大叙事中,其差异化特征便更加凸显。

近年,随着抖音、小红书等内容社区的入局,本地生活战火重燃。然而,无论新旧玩家,其商业逻辑的底层代码似乎并未改变:以内容为载体,商家提供优惠套餐吸引客流,平台撮合交易。这套模式的核心是「信息」与「价格」。

但「小红卡」的订阅制,却提出了一个完全不同的解题思路。在国内互联网用户的习惯里,为本地生活领域的「信息」本身付费,是一个颇具挑战的模式。当其他平台都在用更低的折扣吸引用户时,小红书为何要反其道而行之,设立 168 元的付费门槛?

答案或许就藏在小红书的社区基因里。小红书的用户群体,在决策中对「体验」的看重,往往优先于对「价格」的极致敏感。他们愿意为更独特的氛围、更优质的服务支付溢价。「小红卡」的设计,精准地捕捉了这一核心诉求。

首先,它极大地优化了交易体验。无论是作为活动门票,还是日常的一口价折扣,都避免了传统团购繁琐的比价、凑单、核销流程。这种「无感」的顺滑体验,本身就是一种价值。

小红卡精选摆在商家门店的入口丨来自:极客公园

其次,它强化了社区的信任闭环。「小红书精选」的背后,是平台用自身信誉为用户的交易决策背书。当用户通过小红卡完成交易后,他们更有动力去分享真实体验,从而产生更多高质量的「交易后笔记」。这些笔记不再是泛泛的「种草」,而是带有真实交易验证的「拔草」反馈,让整个「种草-交易-分享」的链条变得更可信、更闭环。

事实上,小红书做本地生活,最大的优势并非流量,而是基于真实分享建立起来的社区信任和独特的评价标准——这家店的「出片率」高吗?主理人的审美如何?这些难以被量化的「体验」价值,恰恰是小红书的护城河。「小红卡」,正是将这套无形的评价体系进行商业变现的一次大胆尝试。它试图告诉市场:在小红书,最好的本地生活服务,卖点不是折扣,而是品味与体验。

一场前途光明的艰难实验

毫无疑问,「小红卡」是一次极具小红书风格的巧妙实验。它未来的发展,下限有保障,上限充满想象,但也伴随着重重挑战。

从下限来看,168 元的年费设计得相当精巧。对于首批三城的用户而言,仅「马路生活节」的活动权益与开卡礼包,就已显得「物有所值」,这极大地降低了用户的决策门槛。莱昂也坦言,设置付费门槛,也是为了测试用户对这个模式的认可度,「验证用户愿意为什么样的价值付费」。

这张卡的上限,则在于它能否真正成为小红书本地生活商业模式的核心。如果成功,小红书将跳出低价竞争的红海,建立起一个以付费会员为核心的、高粘性的本地生活商业生态。然而,通往理想的终点,前路并不平坦。

首要的挑战在于用户心智的持续教育。当节日的狂欢褪去,「小红卡」的日常价值是什么?精明的用户会发现,许多合作门店在其他平台也提供相近折扣。这意味着,用户为 168 元付出的溢价,买的更多是「精选」带来的信息价值和「会员」身份带来的体验优化。如何让用户长期认可并持续为这种「软价值」付费,是小红书需要回答的核心问题。

其次,是优质商家的拓展难题。那些真正顶级的「好店」,往往不缺客流,也因此缺乏参与打折活动的动力。如果「小红卡」的合作门店,无法持续吸引这些标杆商家,其「精选」的含金量就会受到挑战,从而动摇整个产品的价值根基。

最后,是规模化运营的挑战。目前「小红卡」仅在三城试点,团队可以进行精细化运营。但如果要推广到全国,覆盖数以万计的门店,如何保证「精选」标准不走样,将是对团队组织能力的巨大考验。

从更深层次看,「小红卡」目前更像是「马路生活节」的增值服务包,一个探索更深度商业模式的起点。若要将其真正树立为本地生活的核心,小红书必须想清楚,它如何在用户的日常生活中,更无缝地融入从种草到交易的每一个环节。

结语

无论最终成败,「小红卡」的推出,都是小红书在商业化道路上一次极有意义的实验。它真正的价值,或许不在于短期内能撬动多大的本地生活市场份额,而在于它为小红书指明了一条深化其商业模式的全新路径。

长久以来,如何高效地将巨大的「种草」流量转化为直接交易,是小红书商业化探索中的核心命题。「小红卡」正是为此设计的一个关键漏斗。它通过一个轻量级的付费会员体系,筛选出高意愿度的用户,并用「精选+折扣」的模式降低他们的决策门槛,有力地推动用户从「Mark」笔记到完成线下交易这「惊险一跃」。

更具想象力的是,这套以订阅服务为漏斗的模式一旦在本地生活这个高频、复杂的领域被验证成功,将具备极强的可复制性。未来,无论是在二次元游戏、潮流电商还是其他小红书具备内容优势的垂直领域,都可以借鉴「小红卡」的逻辑,推出类似的会员服务,搭建起从内容种草到交易决策的桥梁。

这最终将导向一个对小红书而言最为理想的商业闭环:以优质内容吸引用户,用会员服务促成交易,而交易完成后产生的真实反馈,又将成为社区里最宝贵的优质内容,进一步巩固社区的信任与活力。「小红卡」的征途才刚刚开始,但它所承载的,或许是小红书构建下一代社区商业生态的关键钥匙。

优艾智合赴港IPO :冲刺“移动操作机器人第一股”

2025年9月26日 22:12
9月26日,合肥优艾智合机器人股份有限公司(下称“优艾智合”)正式向香港联合交易所递交上市申请,冲刺“移动操作机器人第一股”。 据官方介绍,优艾智合是全球最早探索具身智能技术并率先实现规模应用的机器人厂商之一。根据弗若斯特沙利文报告,按2024年收入计,优艾智合是全球第一的工业移动操作机器人企业,同时在中国移动操作机器人企业中位居第一。

当 5 亿玩家涌入 AI 的 3D 新世界

2025年9月26日 22:02

在 AI 技术浪潮狂飙的 2025 年,市场的聚光灯无疑主要打在了 AI Agent 这位年度主角身上,它所预示的自动化与智能交互的未来,吸引了绝大部分的目光与资本。

但在同样飞速演进的多模态生成领域,AI 3D 生成是一支被很多人忽略、但即将迎来价值爆发的「潜力股」。它不像大语言模型那样直接与我们对话,也不像文生视频那样在社交媒体上病毒式传播,但它所解决的,是一个数字化时代最基础也最「昂贵」的问题之一:3D 内容的生产。

3D 内容生产的价值不言而喻,从电影特效、电子游戏,到工业设计、虚拟现实,它构成了我们数字世界的骨架。然而,这个骨架的搭建成本却高得惊人。一个游戏角色模型,从概念设计、高精度建模、拓扑、UV 展开、贴图绘制到最终的骨骼绑定,往往需要一个专业资产团队数周甚至数月的时间,成本动辄数十万。这种高昂的门槛,不仅限制了专业内容的生产效率,更将数以亿计的普通用户彻底挡在了 3D 创作的大门之外。

过去,AI 3D 技术虽然被寄予厚望,但受限于模型算法的成熟度,始终停留在「未来可期」的实验室阶段,生成的模型往往是「能看不能用」的数字玩具。但在过去一年左右的时间里,行业落地明显加速。我们观察到三大明确且强烈的市场信号,它们共同指向一个结论:AI 3D 技术已经越过临界点,正以惊人的速度从幕后走向台前,一场关于 3D 内容生产的范式革命,已经悄然开始。

一、3D AI 生成:一个正在加速的赛道

判断一项技术是否真正走出实验室,最好的方式是观察它是否在真实产业中落地并创造价值。今天,AI 3D 正在产业端、用户端和商业端,都在悄悄加速。

首先,最明确的信号来自游戏产业——AI 3D 正在掀起一场「无声的革命」。

游戏,作为对 3D 内容需求量最大、品质要求最苛刻的产业,成为了检验 AI 3D 技术成熟度的第一块,也是最硬的试金石。在今年的 ChinaJoy 展会上,如果你有机会和一线游戏制作人或美术总监深入交流,会发现一个惊人的共识正在他们之间迅速流传:「如今行业内至少一半的美术资产管线,都已经有 AI+3D 技术的深度参与。」

这种参与并非流于表面。它已经深入到游戏开发的毛细血管中:从概念设计阶段快速生成大量 3D 草稿以探索视觉风格,到量产阶段由 AI 辅助生成海量的场景道具、植被、建筑等填充物,再到角色原型设计和贴图纹理的智能生成。AI 正在将美术设计师从大量重复、繁琐的「体力劳动」中解放出来,让他们能更专注于创意本身。

这场变革对产业格局的影响是深远的。它极大地降低了内容制作的成本与周期,甚至成为了一些现象级产品成功的关键变量。2024 年初引爆全球的《幻兽帕鲁()》,其背后的小团队若非借助 AI 工具极大地提升了美术资产的生产效率,几乎不可能在有限的预算和时间内,构建出一个如此丰富且庞大的开放世界。AI 3D,正在让「小团队创造大世界」的梦想成为现实。

第二个信号,是AI 3D 跨越专业鸿沟,开始大规模向 C 端消费市场渗透。

如果说 B 端的工业级应用验证了技术的「下限」——即质量与效率,那么 C 端的普及则决定了生态的「上限」——想象空间与用户规模。今年,AI 3D 在两个不同行业,悄然点起消费市场的火花。

一方面,是消费级 3D 打印机的空前火热。以拓竹、创想三维为代表的厂商,将原本昂贵且复杂的 3D 打印设备,变成了人人都能拥有的桌面工厂。硬件的普及,催生了对海量、个性化 3D 模型的巨大需求。过去,用户只能在模型网站上下载别人设计好的作品,而现在,AI 3D 技术让「所想即所得」成为可能——任何一个普通用户,都可以将自己的创意、一张随手画的草图、一张喜爱的动漫角色画片,通过 AI 转化为可供打印的 3D 模型。

另一方面,则是更具标志性的事件——AI 3D 技术首次与国民级 UGC 平台实现深度融合。近期,AI 3D 领域的领军企业 VAST 宣布与网易旗下的《蛋仔派对》达成合作,将其核心产品 Tripo 大模型,植入了游戏的 UGC 创作系统。

文字/图片生成 3D 模型丨来自:《蛋仔派对》

《蛋仔派对》拥有超过 5 亿的注册用户,其 UGC 社区「蛋仔工坊」本身就是一个巨大的创作生态。在过去,玩家的创作主要依赖于官方提供的预制组件进行「搭建」,更像是「乐高」模式。而现在,任何一位玩家,无论是否具备建模知识,都可以通过「一句话+一张图」的极简方式,生成自己想要的、独一无二的 3D 模型,并直接在自己创造的游戏地图中使用。

这是一次彻底的范式革命。它将 UGC 的本质从「组合」推向了「生成」,是从 0 到 1 的创造。当 5 亿玩家都能轻松地将自己的奇思妙想注入这个虚拟世界时,其内容生态的爆发力将是指数级的。

第三个,也是最硬核的信号:赛道头部公司已经真正赚到钱了。

一个产业能否健康发展,最终要看其商业模式能否形成闭环。在众多 AI 应用赛道还在「烧钱换未来」的困局中,AI 3D 已经展现出惊人的商业化潜力。

据市场消息,VAST 的年度经常性收入(ARR)已突破 1200 万美元。这是一个什么概念?在垂直类的 AI 应用中,这是一个极为罕见且亮眼的数字。横向对比其他 AI 细分领域的头部公司,很少有产品能在如此短的时间内,仅凭一个 50 人左右的精干团队,就达到千万美元级别的 ARR。

这个数字的背后,是清晰的技术价值和强烈的市场需求。对于游戏公司或工业设计企业而言,订阅 AI 3D 服务的费用,可能远低于雇佣一两位美术设计师的年薪,但换来的却是整个团队生产效率的成倍提升。这种极高的 ROI,很容易让行业付费。而产业的深度融合、消费市场的全面破圈、头部企业的商业成功——三大信号叠加,清晰地勾勒出 AI 3D 赛道从量变到质变的发展轨迹。

二、从实验室到产业:一条技术栈的成熟之路

产业落地的突然加速,并非无源之水。其背后,是 AI 3D 生成技术栈历经多年发展,在算法、数据和产品层面均取得决定性突破,最终走向成熟。VAST 旗下 Tripo 大模型的发展史,是整个行业技术演进的缩影。

AI 3D 的早期探索,如 NeRFs(神经辐射场)等技术,虽然惊艳,但更像是「3D 照片」,模型难以编辑、生成速度慢,无法直接应用于工业流程。VAST 的早期模型也处于类似阶段,拥有数亿参数,能初步实现「文字到 3D」的功能,但生成的模型在精度、拓扑结构和纹理细节上,都还有提升空间。

为此,VAST 在过去几年持续迭代模型和技术栈:不仅要让模型「看懂」三维世界,更要让它「理解」三维世界的构建逻辑。

算法层面,其 Tripo 3.0 版本将模型参数规模一举提升至 200 亿,并采用了自研的 SparseFlex 稀疏建模技术。这一创新的意义在于,它让模型能以高达十亿体素级的分辨率来理解和生成 3D 空间。这好比将数字世界的「分辨率」从模糊的 480p 一夜之间提升到了纤毫毕现的 8K IMAX 级别,其精度首次达到了可直接用于游戏角色建模、工业零件设计的严苛标准。

学术与开源层面,VAST 的技术实力也获得了广泛认可。其核心团队成员在 Siggraph、ICCV 等计算机图形学顶级会议上发表了一系列高质量论文,为行业贡献了坚实的理论基础。更重要的是,他们选择将部分核心技术回馈社区,其开源的 TripoSR 等基础模型,因其出色的效果和易用性,迅速获得了全球开发者社区的积极响应,成为备受关注和广泛应用的开源项目之一。

最终,成熟的技术需要一个强大的产品载体来释放其生产力。VAST 推出的Tripo Studio 工作台,正是承载这一切的关键。它并非一个简单的「生成按钮」,而是深度洞察了传统 3D 美术师工作流的痛点后,打造的一站式解决方案。它将传统流程中最为耗时、最繁琐的建模、拓扑(Retopology)、UV 展开、贴图绘制和骨骼绑定等环节,通过 AI 进行了重塑。

拥有 5 亿玩家的《蛋仔派对》丨来自:《蛋仔派对》

例如,「智能低模生成」功能,能自动将超高精度的模型转化为可在手机等低功耗设备上流畅运行的轻量化模型,解决了《蛋仔派对》这类国民级手游的核心需求;而「万物自动绑骨」功能,则让原本需要专业动画师数天才能完成的骨骼绑定工作,在几分钟内自动完成。

正是这样从底层算法突破、顶级学术和开源社区验证,到最终完成产品化整合,才共同构筑了今天 AI 3D 产业发展的地基。

三、VAST 的商业蓝图与终极想象

3D 生成技术,正在从专业开发者的工具,走向普通人的玩具。VAST 正在描绘一张全新的商业蓝图,展示 AI 3D 的可能性。

它的起点,是一条极为务实且精准的商业路径:先服务最专业的PGC(专业生成内容)用户,再降维渗透到最广阔的UGC(用户生成内容)市场。

在发展初期,VAST 聚焦于付费意愿和能力都最强的头部企业客户。通过与网易、腾讯、索尼等行业巨头的深度合作,VAST 不仅获得了早期宝贵的现金流和高达 1200 万美元的 ARR,更重要的是,在这些最严苛、最前沿的工业场景下,其技术得到了反复的打磨和验证。这些 PGC 客户的需求,如同「陪练」,倒逼着 VAST 的技术能力不断突破。

但 VAST 的野心显然不止于成为一个服务于少数金字塔尖用户的「效率工具」。它的下一步,是指向更广阔的星辰大海——赋能每一个普通人,开启 UGC 的新纪元。

这里最值得借鉴的,或许就是「剪映之于抖音」的颠覆性模式。

在剪映出现之前,视频剪辑是少数专业人士才能掌握的复杂技能,PR 和 AE 的学习曲线令人望而却步。但剪映通过提供海量模板、一键式特效和极其简单的操作界面,将视频创作的门槛降低到了前所未有的程度。正是因为有了剪映,亿万普通人才得以将自己的生活点滴和创意火花,转化为生动的短视频,从而极大地繁荣了抖音的内容生态,并最终成就了字节跳动的商业帝国。

VAST 正在做的,正是在 3D 世界里,扮演「剪映」的角色。它的价值,不仅是帮助专业公司提效,更是通过 API、平台化产品等多种方式,将 3D 创作的门槛无限降低,直到普通人也能轻松上手。

我们已经看到了这一蓝图的初步例证:在《蛋仔派对》里,孩子的涂鸦可以一键变为游戏里可交互的奇特萌宠;在 3D 打印社区,一个设计师的灵感草图能快速生成可触摸的实体模型。这些,只是一个开始。

玩家用 3D 模型构建自己的地图丨来自:《蛋仔派对》

长远来看,这项技术的想象空间,将远远超出游戏和打印的范畴。

当我们的技术范式真正迈入空间计算和元宇宙的时代,当数字内容不再局限于二维屏幕,人与 3D 信息的交互将成为日常。届时,无论是构建自己的虚拟家园,为电商产品进行个性化设计,还是在教学、医疗领域创建交互式模拟,我们都需要一种能力——快速、低成本地将想法转化为三维实体。

AI 3D 生成,正是通往这个未来的底层技术基石。它所开启的,不仅仅是一个降本增效的工具时代,更是一个创造力被极大解放的新纪元。从这个角度看,VAST 与《蛋仔派对》的合作,或许不仅仅是一次成功的商业落地,更是一个重要的隐喻——它预示着,那个任何人都能随心所欲创造自己 3D 世界的时代,真的要来了。

我们离失去“睡觉自由”,还有多远?

2025年9月26日 21:00

想象一下不用睡觉,24小时精力满格,是不是很爽?在这个全天候高速运转的社会,我们是否还拥有“休息”的权利?如果人类不再需要睡觉,世界会变成什么样?

下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

名创优品:拟以股份于香港联交所主板独立上市的方式分拆附属公司TOPTOY

2025年9月26日 20:59
36氪获悉,名创优品公告,公司拟以TOPTOY股份于香港联交所主板独立上市的方式分拆附属公司TOPTOY。公司已根据第15项应用指引就建议分拆向香港联交所提交分拆建议,且香港联交所已确认公司可进行建议分拆。于2025年9月26日,TOPTOY透过其联席保荐人向香港联交所提交上市申请表格(A1表格),申请批准TOPTOY股份于香港联交所主板上市及买卖。现建议以全球发售及分派的方式进行建议分拆。建议分拆完成后,TOPTOY将仍为公司的附属公司。

楖览: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 辅助生成,并由作者整理审核。

富煌钢构:因涉嫌信息披露违法违规被中国证监会立案

2025年9月26日 20:42
36氪获悉,富煌钢构公告,公司当日收到中国证监会的《立案告知书》,因涉嫌信息披露违法违规,中国证监会决定对公司进行立案。目前公司各项经营活动和业务均正常开展,在立案调查期间,公司将积极配合中国证监会的相关调查工作,并严格按照有关法律法规及监管要求履行信息披露义务。

氪星晚报|挪威航空将增购30架波音737飞机;淘宝Vision未来旗舰店上海首店开业;美国配饰零售商Claire's或关闭数十家英国门店

2025年9月26日 20:36

大公司:

骄成超声:在功率半导体领域的全工序超声波解决方案均已实现批量出货

36氪获悉,骄成超声发布投资者关系活动记录表,在功率半导体领域,公司有超声波端子焊接机、超声波PIN针焊接机、超声波键合机、超声波扫描显微镜等全工序超声波解决方案,并均已实现批量出货。在该领域,公司与上汽英飞凌、中车时代、振华科技、宏微科技、士兰微、芯联集成等知名企业保持良好合作。

消息称英特尔除Jaguar Shores外还在开发另一款数据中心GPU

据CRN,熟悉英特尔计划的消息人士披露,英特尔除 "Jaguar Shores" 外还在开发另一款数据中心GPU。这一新的显卡尚未对外公布,对服务器的功耗需求更低,可能于明年某个时间点面世。

挪威航空将增购30架波音737飞机

9月26日,挪威航空发表声明称,将通过行使增购选项,将现有波音订单扩大至额外采购30架波音737 MAX 8飞机,使总确认订单量达到80架。为配合此次增购,挪威航空与波音公司已就调整部分交付日期达成协议,该订单最后一批飞机现定于2031年交付。

淘宝Vision未来旗舰店上海首店开业

36氪获悉,9月26日,淘宝Vision未来旗舰店上海首店开业。据了解,淘宝Vision未来旗舰店是淘宝最新打造的未来购物空间,通过“AI+3D+XR”技术的融合,为品牌构建一个提升线下门店效率的解决方案,支持品牌在有限空间里无限展示SKU。

赛诺菲在美推出胰岛素“月付35美元”方案

赛诺菲美国公司9月26日宣布,其“胰岛素储蓄计划(Insulins Valyou Savings Program)”将大幅扩展覆盖范围,凡持有有效处方的美国患者,均可35美元购买任何赛诺菲胰岛素30天用量。该计划最初为无医保人群设立,现将面向所有美国民众开放,无论其保险状况如何——包括商业保险或联邦医疗保险参保者。扩大后的“胰岛素储蓄计划”现确保每位美国患者每月支付的赛诺菲胰岛素费用不超过35美元。

美国配饰零售商Claire's或关闭数十家英国门店

据报道,英国高街连锁品牌WH Smith的收购方——Modella Capital,正接近达成一项协议,以拯救美国配饰零售商Claire's的英国业务。然而,该交易仍可能导致数十家门店关闭。

新产品:

日产汽车计划在2027财年内推出集成人工智能技术的下一代ProPILOT超智驾

据日产汽车官方消息,日产汽车已于本月启动其下一代ProPILOT超智驾(高级驾驶辅助技术)的路测演示,该系统计划于2027财年内在日本正式推出。基于纯电动车型日产Ariya艾睿雅打造的原型车队,在东京市中心的复杂城市路况中展现了安全可靠的驾驶能力*。下一代ProPILOT超智驾系统融合了Wayve(一家英国自动驾驶技术研发公司)的人工智能驾驶软件(Wayve AI Driver)与日产汽车基于下一代激光雷达的“地面实况感知”技术,旨在为驾驶辅助技术设定新的标准。

影石Insta360 Wave AI录音全向麦克风发布

36氪获悉,9月25日,影石Insta360推出全新Wave AI录音全向麦克风,该产品深度融合腾讯天籁inside解决方案,针对线上会议中噪声干扰、远距离拾音不清、音视频协同难等痛点,实现300余种环境噪声消除、5米远距离超宽带清晰拾音,并支持AI自动生成多模板会议纪要与5分钟预录功能。

投融资:

“象生科技”连续完成两轮数千万元天使+轮融资

36氪获悉,“象生科技”近期宣布连续完成两轮数千万元天使+轮融资,由产业方CVC美丽境界资本、毅达资本、纳川资本联合领投,英诺天使基金、苏州市科创投、苏州天使母基金、苏高新科创天使基金等机构跟投,资金将用于AI催化技术平台建设、产品研发、产线拓展及智能工厂建设。

今日观点:

宇树科技CEO王兴兴:下半年将发布身高1.8米的人形机器人

宇树科技创始人、CEO王兴兴9月26日在第四届全球数字贸易博览会上发言称,宇树科技机器人算法今年已经历几次迭代,预计下半年将发布身高1.8米的人形机器人。他表示,今年上半年国内机器人行业发展火热,“中国智能机器人相关企业平均增长率达50%到100%”。前段时间宇树科技再次更新了算法,使机器人稳定性大幅提升。“我们把它定义为反重力模式,基本上机器人在任何干扰下都能自己恢复站立。算法升级后,理论上目前机器人可以完成各种舞蹈动作、武术动作。”王兴兴说。

其他值得关注的新闻:

市场监管总局召开促进网络餐饮外卖行业健康发展座谈会

36氪获悉,市场监管总局召开促进网络餐饮外卖行业健康发展座谈会。会议强调,近年来我国网络餐饮外卖行业正展现出强劲的发展势头和广阔的市场前景,但在满足消费者多样化需求的同时,也对市场竞争秩序、食品安全、相关主体权益保障等提出更高要求。下一步,市场监管部门将深入贯彻落实党中央、国务院决策部署,提升网络餐饮外卖领域常态化监管水平,加大监管执法力度,督促相关主体理性参与竞争、落实食品安全主体责任,构建行业良好生态。

特朗普单方面宣布药品高额关税,欧美贸易协议再陷混乱

当地时间9月26日,欧盟委员会称已同意对美国药品关税设定15%的上限,为欧洲企业提供保障。但美国总统特朗普此前已于美国当地时间25日宣布,将自10月1日起对进口的所有品牌或专利药品征收100%关税,再次令今年8月刚达成的美欧药品关税协议陷入混乱。

我国在北极冰区首次实现载人深潜

执行中国第15次北冰洋科学考察任务的“雪龙2”号极地科考破冰船9月26日回到上海。在“雪龙2”号保障支持下,“深海一号”搭载“蛟龙”号载人潜水器成功完成我国载人深潜北极冰区首次下潜,标志着我国深海进入和深海探测能力持续增强。自然资源部组织的中国第15次北冰洋科学考察,由“雪龙2”号、“极地”号、“深海一号”和“探索三号”四船共同实施,是我国规模最大的北冰洋科学考察。

中方再次就TikTok问题表明立场

9月26日,外交部发言人郭嘉昆主持例行记者会。有记者就TikTok相关问题提问。对此,郭嘉昆表示,我要重申,中方在TikTok问题上的立场是清楚的。中国政府尊重企业意愿,乐见企业在符合市场规则基础上做好商业谈判,达成符合中国法律法规、利益平衡的解决方案。希望美方为中国企业到美国投资提供开放、公平、非歧视的营商环境。

23.98 万元,理想 i6 登场:便宜 10 万,年轻 10 岁

作者 芥末
2025年9月26日 20:35

经历过 i8 的铺垫,理想 i6 的登场更像是一场开卷考试。

不同于以往高中低三种配置,理想 i6 仅推出了一个 24.98 万元的版本,定价干脆利落。

首销期赠送空气悬架、冰箱和电吸门,后排娱乐屏、流媒体后视镜、拖钩、四驱和铂金音响,则留给用户按需选配。

再加上 1 万元的现金优惠,理想 i6 整整比 i8 便宜了 10 万元——堪称理想迄今最具性价比的车型。

最年轻好开的理想

理想 i6 的车身尺寸为 4950 × 1935 × 1670 mm,轴距 3000 mm,比 i8 小了一圈,与小米 YU7 接近,采用 2+3 的五座布局。

虽然只少了一个座位,但定位却发生了根本转变:i8 依旧主打「家庭出行」,而 i6 则开始更强调车主的个人感受。无论是宣传语还是场景设定,理想对 i6 这款车的定位,都从家庭化转向旅游、运动和派对,充满年轻与自我表达的意味。

基于这样的目标人群和使用场景,理想对 i6 的细节设计进行了调整。

设计 i6 的时候,我们要做一个世界上没有出现过的产品,第一不能太大,车长要克制,要跟 L6 一样好开,但又要很大,座舱空间跟 L7 一样大,第二要做到行业领先的低风阻、低能耗、安静,第三是必须足够运动,造一台最好开的理想。

虽然大家普遍会觉得 i6 是 i8 的小号版本,但细细看来,虽然整体设计言语保持了一致,但 i6 整体上显得年轻许多,短前后悬+长轴距的组合以及原生双色车身、低窗线的设计,让 i6 整体的姿态灵动了不少。

在车尾的处理上,i6 明显也比 i8 更有层次,运动感也更强。

如果再配上理想官方的行李架、拖挂套装和自行车架,i6 身上这种「玩乐感」氛围会来的更强。

这样的车身设计也为 i6 带来了更低的风阻系数,搭配上理想自研的 93.08% 综合效率的电机和 87.3kWh 的磷酸铁锂电池,i6 的百公里能耗约为 13.6 kWh, CLTC 综合续航 720km,峰值充电功率可达 500kW,充电 10 分钟可补能 500 公里。

理想 i6 上还搭载了行业独有的「双呼吸」冷却系统,该系统能够有效控制充电时的电池温度,保证电池寿命,让车辆在在低电量状态下也拥有优异的放电性能,0% 电量时仍可稳定维持 120 公里/小时巡航,并且能够正常爬坡、出地库。

理想甚至在发布会上用了不短的篇幅来介绍 i6 的操控性。

硬件方面,理想 i6 整车扭转刚度为 52014 N·m / deg,配备前双叉臂和后五连杆独立悬架,并采用全铝摆臂来降低簧下质量,选配四驱版本后,零百加速可以提升至 4.5 秒。

此外,理想 i6 还配备了四个高性能液压衬套,以及CDC连续可变阻尼减振器和双腔魔毯空气悬架,可以根据不同场景,灵活选择两种悬架刚度和四档高度。

在减振器调校上,理想i6采用了与赛车相同的大复原、低压缩的调校方式,配合全栈自研的智能电控底盘系统,即便在山路弯道中行驶,车身依旧能保持出色的整车抓地力。

在国际标准高速紧急避障测试(ISO-3888-1)中,理想 i6 的成绩达到了同级别最高的 130 公里/小时。

一脉相承的智能安全

走进车内,理想 i6 的座舱内部除了座椅布局,几乎和理想 i8 一模一样。

新车在 4950mm 的车身长度和 3000mm 的轴距下,通过低重心和短前后悬设计,使车内的纵向可用空间超过了 3.3 米。

舒适性性配置上则提供了 8.8 升智能冷暖双用冰箱、21.4 英寸 3K 后舱娱乐屏、铂金音响、侧窗隐私帘等,除了零重力座椅外,几乎和 i8 一模一样。

理想 i6 支持全车四座座椅通风,五座座椅加热,前排标配双躺椅,坐垫长度为 51cm,同时二排标配「皇后座」,在一排座椅向后放倒后,i6 能够在两侧各腾出一张长度超 1.7 米的大床。

而在储物空间表现上,理想 i6 号称是「个性化的移动车库」。理想 i6 后备厢进深为 1.1米,可以轻松收纳三个 28 寸行李箱和两个 20 寸行李箱,后备厢还配备带有盖板的下沉式储物空间。

此外,理想 i6 标配前备箱,空间可容纳一个背包及一个标准尺寸登机箱,前备箱配备高位照明灯和排水孔,采用低开口设计,能够更便捷轻松的拿取物品。

新车在智能化水平上的表现也保持了理想一贯的高水平。

在前排的车机部分,理想 i6 延续了理想家族式 3K 双联屏设计,内置了基于理想汽车自研的 MindGPT 模型打造的理想同学智能体。

在实际使用中,理想同学具备超低延迟的理解和推理能力、超长记忆和高表现力的语言生成能力,是行业中唯一支持生成自主方言的模型。此外,理想同学智能体还能实现定制开发桌面卡片、自动停车缴费等任务。

理想 i6 全系标配了激光雷达,搭载基于 VLA 司机大模型的 AD Max 高级辅助驾驶,在使用过程中可以和驾驶者实时交流,像人一样能够听得懂指令,与驾驶者进行沟通,并记住驾驶习惯。

在安全能力上,理想 i6 搭载了全速域 AEB 自动紧急制动和 AES 自动紧急转向,搭载了三腔远端气囊。新车在开发过程中使用超过 100 项碰撞开发矩阵进行整车级测试,车辆使用的 5C 磷酸铁锂电池提前满足了所有的新国标测试项目。

理想造了这么多年的大体型 SUV,基本上都走的是家庭舒适风,i6 是第一款试图以好开和情绪价值来打开市场的产品。

新形态「理想 i6」关注年轻一代的自我表达,从「车定义人」转变为「用车去表达自己」,不仅解决年轻用户的出行痛点,更懂年轻人的出行痒点。

但要在 20-30 万元纯电中大型 SUV 市场中做出成绩,理想 i6 注定要和特斯拉 Model Y、小米 YU7、乐道 L90 等爆款车型同台竞技。

为了给 i6 造势,理想汽车甚至打破惯例,选择了易烊千玺作为首位代言人,想要借助代言人的影响力帮助理想 i6 快速破圈。

在昨晚的年度演讲中,雷军也推荐了理想 i6:

理想开创了冰箱彩电大沙发,大空间增程 SUV 品类,不少厂商就摸着理想过河,活得也不错,大家不买 YU7,明天理想 i6 发布也不错,也可以看看。

一直被别人摸着过河的理想,现在为了进入年轻人市场,也开始反过来摸着小米过河了。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


嘉泽新能:公司绿色化工业务尚处于论证、起步阶段,尚不具备商业化的条件

2025年9月26日 20:32
36氪获悉,嘉泽新能公告,截至目前,公司绿色化工业务尚处于论证、起步阶段,尚不具备商业化的条件,短期内对公司的营业收入、利润增长没有实质或重大影响。公司绿色化工业务在实施过程中可能面临政策风险、市场风险、行业竞争风险、产品价格波动以及各种因素导致效益不达预期的风险。公司将按照法律法规的相关要求,及时披露后续进展情况,敬请投资者注意投资风险。
❌
❌