Skip to content
签到签到

第 9 章:React-free 对象层与快照

本章解决的问题

前端开发者容易把所有状态塞进 React。DeepSeek Harness 的 Web 客户端把业务逻辑放在 React 外,再通过快照喂给 React。这一章解释这套设计。

交互图解:React-free 前端流式运行时 · 查看图解导读

大白话理解

如果每个 token 都触发一次 setState 并让整个聊天列表重渲染,页面会非常慢。

更好的方式是:

text
业务对象层(无 React)
-> 累积流式状态
-> 生成不可变快照
-> 合批通知
-> React 订阅快照

这样业务逻辑不依赖 React,React 只负责投影。

必须掌握的概念

1. React-free Object Layer

对象层负责:

  • 连接管理。
  • 事件接收。
  • 流式累积。
  • 工具调用树。
  • 会话状态。

它不 import React。

2. Immutable Snapshot

业务对象生成一个不可变快照。相同数据返回相同引用,变化时返回新引用。

3. useSyncExternalStore

React 通过 subscribegetSnapshot 订阅外部 store。

4. Notification Batching

多个快速更新可以合并成一次通知,避免每来一个 token 就通知一次。

DeepSeek Harness 对应实现

DeepSeek Harness 的 Web 客户端有三层:

text
runtime object layer
-> web-react glue
-> pure props components

关键类:

  • SessionManager 管理多个会话。
  • Session 管理单个会话的事件和快照。
  • ConversationNodeAssembler 把事件组装成对话节点。
  • Notifier 合并通知。

React 使用 useSyncExternalStore 订阅 Session.getSnapshot()

进一步阅读:

  • .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
  • packages/client/runtime/src/client/sessions/session.ts

代码示例

一个最小 Observable Snapshot

ts
type Listener = () => void

class StreamingSession {
  private text = ''
  private snapshot = { text: '', done: false }
  private listeners = new Set<Listener>()

  append(delta: string): void {
    this.text += delta
    this.snapshot = { text: this.text, done: false }
    this.emit()
  }

  complete(): void {
    this.snapshot = { text: this.text, done: true }
    this.emit()
  }

  getSnapshot() {
    return this.snapshot
  }

  subscribe(listener: Listener) {
    this.listeners.add(listener)
    return () => this.listeners.delete(listener)
  }

  private emit() {
    for (const listener of this.listeners) listener()
  }
}

React 订阅

tsx
import { useSyncExternalStore } from 'react'

function StreamingView({ session }: { session: StreamingSession }) {
  const snapshot = useSyncExternalStore(
    callback => session.subscribe(callback),
    () => session.getSnapshot(),
  )

  return (
    <div>
      <p>{snapshot.text}</p>
      <p>{snapshot.done ? 'done' : 'streaming'}</p>
    </div>
  )
}

useSyncExternalStore 会帮助 React 避免在相同快照引用下重复渲染。

面试怎么问

问:为什么要把业务状态放在 React 外?

回答要点:

  • 流式输出频率高。
  • 业务状态机不应该绑死 React。
  • 便于测试和复用。
  • 可替换 UI 框架。

问:useSyncExternalStore 解决什么问题?

回答要点:

  • 订阅 React 外部的可观察状态。
  • 支持并发渲染。
  • 快照引用稳定时可避免不必要更新。

问:多个 token 到达时如何合批?

回答要点:

  • 对象层累积状态。
  • 在 microtask 或 animation frame 中重建快照。
  • 一次性通知订阅者。
  • 避免每个 token 触发一次 React 更新。

自测题

  1. React-free object layer 的职责是什么?
  2. getSnapshotsubscribe 的约定是什么?
  3. 为什么不可变快照对 React 性能重要?
  4. 什么时候应把状态放对象层,什么时候应放组件 local state?

本章小结

DeepSeek Harness 的前端架构值得重点学习。它把高频流式业务隔离在 React 外,用不可变快照和 useSyncExternalStore 连接视图。

进一步阅读:

  • packages/client/web-react/src/scoped-slots.tsx
  • .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md