tldraw 状态管理内核 @tldraw/state 架构深度解析:基于信号(Signals)的响应式系统
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
@tldraw/state 是 tldraw 开源仓库中负责响应式状态管理的核心包,它用一套基于信号(signals)的细粒度响应式原语,驱动 tldraw 画布编辑器从渲染更新到协同状态同步的几乎一切动态行为。本文以 packages/state/ARCHITECTURE.md 为骨架,结合 packages/state/src 下的源码实现与 packages/state/README.md 的实战示例,系统讲解该包的目录结构、核心模块职责、导出 API 映射、依赖图、架构模式与性能优化手段,帮助你读懂这套响应式内核,并能在自己的项目中熟练使用atom/computed/react/transact等原语。
包概览:一个轻量、框架无关的信号库
根据 ARCHITECTURE.md 的定义,@tldraw/state是一个使用信号(signals)实现响应式状态管理的 TypeScript 库:
- 包名:
@tldraw/state - 定位:响应式状态管理库,核心是信号(signals)
- 入口文件:packages/state/src/index.ts,导出完整的响应式信号 API
- 依赖:
@tldraw/utils(主要用到registerTldrawLibraryVersion()、assert()等工具)
它"轻量"而"强大"的关键在于细粒度响应式:系统自动跟踪依赖,只重新计算真正变化的部分。README 中总结了它的几个核心卖点:
- 细粒度响应式:只在实际依赖发生变化时重跑计算;
- 高性能:惰性求值(lazy evaluation)与高效的依赖跟踪;
- 自动更新:派生值与副作用随状态自动更新;
- 增量更新:内置 diff 历史与支持回滚的事务(transaction);
- 框架无关:可配合任意 JS 框架或纯 JavaScript 使用;
- TypeScript 优先:完整的类型支持。
该包支撑着 tldraw 的响应式系统核心(画布更新、协同状态同步等),并对外提供可选的 React 绑定包 packages/state-react 与记录存储包 packages/store。
从 packages/state/src/index.ts 可以看到一个有趣的细节:入口文件会通过singleton('apiVersion', ...)校验当前 API 版本,若检测到应用中存在多个不兼容的@tldraw/state副本,会直接抛错提示去重——这是对"重复打包导致全局状态分裂"这类经典问题的防御。
目录结构:一份 AI 友好的源码地图
ARCHITECTURE.md 提供了一份面向机器与 Agent 的目录结构说明,实际源码布局与之完全对应:
packages/state/ ├── src/ │ ├── index.ts # 主入口,导出全部公开 API │ └── lib/ # 核心实现模块 │ ├── Atom.ts # 可变响应式状态容器(atom) │ ├── Computed.ts # 派生响应式值(computed,惰性求值) │ ├── EffectScheduler.ts # 副作用与反应(effect / reaction) │ ├── ArraySet.ts # 优化的集合数据结构 │ ├── HistoryBuffer.ts # 变更追踪与历史(环形缓冲) │ ├── capture.ts # 依赖追踪系统 │ ├── transactions.ts # 批量更新与回滚、全局 epoch、反应阶段(flushChanges) │ ├── types.ts # 核心类型定义(Signal、Child、ComputeDiff、RESET_VALUE) │ ├── constants.ts # 系统常量(如 GLOBAL_START_EPOCH) │ ├── helpers.ts # 工具函数(equals、singleton、attach、detach、haveParentsChanged) │ ├── isSignal.ts # 类型守卫 │ ├── isComputed.ts # computed 品牌检查(刻意不 import Computed.ts 以避免循环依赖) │ ├── localStorageAtom.ts # 持久化到 localStorage 的 atom │ ├── warnings.ts # 开发期警告 │ └── __tests__/ # 测试文件 ├── DOCS.md # 面向人类的 API 文档 ├── README.md # 包简介与快速上手 ├── SPEC.md # 行为规范(测试引用的规则 ID) └── ARCHITECTURE.md # 本文对应的架构文档值得注意的工程细节:isComputed.ts被刻意设计为不 importComputed.ts,目的就是避免循环依赖(capture.ts 依赖 isComputed,而 Computed.ts 又依赖 capture.ts)。这是该代码库中处理循环依赖的一个典型手法。
核心模块逐个拆解
ARCHITECTURE.md 将核心模块分为"主要 API 模块"与"支撑基础设施"两层,下面结合源码逐一看清每个模块的真实职责。
三个主要 API 模块
Atom.ts —— 可变的响应式状态容器
- 导出:
atom()、isAtom()、Atom接口、AtomOptions - 关键类:
_Atom(内部实现类__Atom__) - 依赖:ArraySet、HistoryBuffer、capture、helpers、transactions、types
Atom 是"直接可写"的信号。它的核心行为在 Atom.ts 的set()中体现:
- 值未变化则跳过:使用
isEqual或默认的equals(===→Object.is→ 旧值上的.equals()方法)判断新旧值是否相等,相等则直接返回当前值(no-op); - 计算 diff:若配置了
historyBuffer,则通过computeDiff(或调用者显式传入的 diff)生成历史条目; - 推进全局 epoch:调用
advanceGlobalEpoch()使全局 epoch +1,并把本次写入归属到该 epoch; - 记录历史并更新值:将 diff 推入
HistoryBuffer,更新lastChangedEpoch,写入新值; - 通知子节点:调用
atomDidChange()触发下游(computed / effect)的失效与重算。
get()中会调用maybeCaptureParent(this)(见 Atom.ts),从而在读取时自动登记依赖关系。此外 Atom 还提供update(updater)便捷方法(等价于set(updater(current)))以及getDiffSince(epoch)获取增量历史。
atom()工厂函数签名(Atom.ts):atom(name, initialValue, options?),其中name用于调试与性能分析、无需全局唯一。
Computed.ts —— 派生响应式值(惰性求值)
- 导出:
computed()、@computed、getComputedInstance()、isUninitialized()、UNINITIALIZED、withDiff() - 关键类:
_Computed(内部实现类__UNSAFE__Computed) - 依赖:ArraySet、HistoryBuffer、capture、constants、helpers、isComputed、transactions、types、warnings
Computed 是"由其他信号派生而来"的信号,其求值核心在 Computed.ts 的__unsafe__getWithoutCapture()中:它首先用lastCheckedEpoch、isActivelyListening与haveParentsChanged()做"脏检查"(dirty check),若依赖未变则直接返回缓存的state;否则在捕获依赖的上下文里运行derive(previousValue, lastComputedEpoch)重算,并通过isEqual判断新值是否真的变化,只有变化时才推进lastChangedEpoch、写入历史。
computed()是一个多重签名的函数(Computed.ts),既可以当作普通工厂computed(name, fn, options?)使用,也可以作为 TC39 装饰器@computed、旧式 TypeScript 装饰器或装饰器工厂@computed(options)用于类方法。内部实现makeComputedWrapper会为每个被装饰的方法在实例上惰性创建并缓存一个_Computed实例(Computed.ts)。
其他导出都围绕增量计算与调试展开:
UNINITIALIZED/isUninitialized():判断 computed 是否是首次运行,首次运行时derive收到的previousValue就是UNINITIALIZED;withDiff(value, diff):让 computed 的计算函数直接返回"值 + diff"的组合,从而在增量计算时顺带增量地计算 diff,避免用computeDiff从旧值新值重新推导;getComputedInstance(obj, prop):取回类实例上某个@computed属性对应的底层 Computed 实例(Computed.ts 沿原型链查找派生存储符号)。
EffectScheduler.ts —— 副作用与反应管理
- 导出:
EffectScheduler、react()、reactor()、EffectSchedulerOptions、Reactor - 关键类:
__EffectScheduler__(reactor()返回的是包一层该调度器的普通对象) - 依赖:ArraySet、capture、constants、helpers、transactions、types
副作用(effect)是响应式系统的"输出端"。react()(EffectScheduler.ts)会创建一个调度器并立即attach()+scheduleEffect(),随后返回一个停止函数;reactor()(EffectScheduler.ts)则返回带start({force})/stop()的可控对象,且停止后重新start()时若依赖未变化则不会重跑。
调度器内部的关键状态包括:
isActivelyListening:是否附着在依赖图上监听变化;lastReactedEpoch/lastTraversedEpoch:记录上次执行与上次遍历的 epoch,用于跳过无效的调度;scheduleCount:被调度次数统计;maybeScheduleEffect()(EffectScheduler.ts):先做一系列"bail out"判断(未监听、epoch 未变、parents 未变),只有确实需要时才真正调度。
EffectSchedulerOptions支持自定义scheduleEffect(execute),典型用途是配合requestAnimationFrame把多个 effect 批量延迟到下一帧执行(EffectScheduler.ts),tldraw 的渲染管线正是靠这种可插拔调度实现的。
支撑基础设施模块
types.ts —— 全系统的类型地基
- 核心类型:
Signal<Value, Diff>、Child、ComputeDiff、RESET_VALUE
Signal接口(types.ts)是所有响应式值的统一抽象,定义了三件套:
get():读取当前值并建立依赖关系;lastChangedEpoch:该信号值最后一次实际变化的全局 epoch(注意:computed 重算但结果相同不会推进它);getDiffSince(epoch):返回自某 epoch 以来的 diff 数组,历史不足时返回RESET_VALUE;__unsafe__getWithoutCapture():读取值但不建立依赖,用于热路径优化。
Child接口(types.ts)描述依赖图上的子节点(computed 与 effect 调度器),记录了parentSet/parents/parentEpochs/isActivelyListening等依赖关系元数据。ComputeDiff是用户自定义的 diff 计算函数签名(types.ts),返回 diff 或RESET_VALUE。RESET_VALUE是一个全局唯一 symbol(Symbol.for('com.tldraw.state/RESET_VALUE')),表示"历史不足、无法计算增量 diff,需要整体重建"。
capture.ts —— 依赖追踪系统
- 导出:
unsafe__withoutCapture()、whyAmIRunning() - 关键类:
CaptureStackFrame - 依赖:helpers、isComputed、types(刻意不依赖 Computed.ts,避免循环)
依赖追踪的核心是一个捕获栈(inst.stack)。当一个 computed 或 effect 开始执行时,startCapturingParents(child)压入一个帧;期间任何信号调用get()都会经由maybeCaptureParent(p)(capture.ts)把该信号登记为当前 child 的父依赖;执行结束后stopCapturingParents()(capture.ts)清理本次未再使用到的旧依赖并截断依赖数组。
unsafe__withoutCapture(fn)通过临时把捕获栈置空来"读值但不产生依赖",适用于"只想让 effect 响应 A 变化、而 B 只是顺带读一下"的场景。whyAmIRunning()则是一个调试利器:在 effect/computed 内部调用后,从下一次运行开始会打印出"因为哪些祖先信号变化而运行"的依赖树(capture.ts)。
transactions.ts —— 批量更新、回滚与反应阶段
- 导出:
transact()、transaction()、deferAsyncEffects() - 关键类:
Transaction - 依赖:Atom、constants、helpers、types
这是系统的"调度中枢",承担三件事:
- 全局 epoch:
getGlobalEpoch()/advanceGlobalEpoch(),任何 atom 变化都会使全局 epoch +1,作为全系统的时间戳; - 事务(Transaction):
transaction(fn)(transactions.ts)总是新建事务并支持嵌套,fn收到一个rollback回调,调用它或让fn抛错都会触发txn.abort(),把本次事务改过的所有 atom 恢复到initialAtomValues记录的事务前状态;transact(fn)(transactions.ts)则"已有事务则复用、没有才新建",是批量更新的首选 API; - 反应阶段(flushChanges)(transactions.ts):事务提交时遍历变更 atom 的子孙,收集需要运行的 reactor,统一执行
maybeScheduleEffect();若 effect 又改了状态,则通过cleanupReactors队列迭代处理(有 1000 层深度上限,防止无限循环)。
事务期间(getIsInTransaction()为真)atom 的变更只会被记录,不会立即遍历子节点,这正是transact能批量合并中间态更新、让 effect 只跑一次的原因。deferAsyncEffects(fn)则是异步事务版本:在异步流程中批量更新状态(如 loading → fetch → data),并明确禁止在同步事务中调用(transactions.ts)。
helpers.ts —— 工具与性能优化
导出:
EMPTY_ARRAY与若干内部工具关键函数:
equals()、singleton()、attach()、detach()、haveParentsChanged()equals(a, b)(helpers.ts):默认值比较逻辑——先===,再Object.is(因此NaN等于NaN),最后尝试旧值的.equals()方法;attach/detach(helpers.ts):在依赖图上增删"父→子"边,并递归传播:computed 获得第一个子节点时开始向上游监听,失去最后一个子节点时递归停止监听,从而保证"没人听的时候整条链路都安静";haveParentsChanged(child)(helpers.ts):O(parents) 的脏检查,遇到第一个变化的父节点就提前返回 true,并且会先把父节点(computed)刷新到最新;singleton(key, init):用Symbol.for('com.tldraw.state/' + key)在globalThis上挂全局单例,实现跨模块共享同一实例;EMPTY_ARRAY:冻结的空数组单例,作为"无 diff / 无依赖"的零分配返回。
ArraySet.ts —— 高性能集合
ArraySet是父-子依赖关系的高效实现:在数组模式下同时用数组(保序、可索引)配合indexOf查找,兼具小规模时的低开销与去重能力,是所有信号children与 child 的parentSet的底层存储。
HistoryBuffer.ts —— 变更历史环形缓冲
- 角色:存储最近的 diff,供
getDiffSince使用
HistoryBuffer(HistoryBuffer.ts)是一个固定容量的环形缓冲,每个条目是[fromEpoch, toEpoch, diff]三元组,要求条目 epoch 区间连续。pushEntry遇到RESET_VALUE或undefined时清空整个缓冲(避免留下缺口导致后续返回不完整的 diff 列表);getChangesSince(sinceEpoch)从最新条目倒序查找覆盖目标 epoch 的区间,历史不足则返回RESET_VALUE。它正是 undo/redo 与增量同步(computed / effect 从 diff 更新而非全量重算)的基础设施。
导出 API 映射:一张"功能 → 入口"速查表
ARCHITECTURE.md 给出的导出映射,在实际 index.ts 中一一对应:
// 核心响应式原语 atom(name, value, options?) -> Atom<T> computed(name, fn, options?) -> Computed<T> react(name, fn, options?) -> () => void reactor(name, fn) -> Reactor<T> // 事务管理 transact(fn) -> T transaction(fn) -> T deferAsyncEffects(fn) -> Promise<T> // 调试与工具 unsafe__withoutCapture(fn) -> T whyAmIRunning() -> void isSignal(value) -> boolean isAtom(value) -> boolean isUninitialized(value) -> boolean // 高级特性 @computed -> PropertyDecorator getComputedInstance(obj, prop) -> Computed withDiff(value, diff) -> WithDiff UNINITIALIZED -> symbol RESET_VALUE -> symbol EMPTY_ARRAY -> readonly array localStorageAtom(name, initialValue, options?) -> [Atom<T>, cleanup] EffectScheduler -> class(底层 effect 运行器) ArraySet -> class(内部使用)其中localStorageAtom(localStorageAtom.ts)值得一提:它返回[atom, cleanup]元组,atom 与 localStorage 双向同步——初始化时若 localStorage 已有值则用其作为初始值(解析失败则清除脏数据);内部用react()把每次变化序列化回 localStorage,并监听storage事件实现跨标签页同步(其他标签页的写入会实时更新本标签页的 atom),同时兼容无window的环境(Node / SSR)。
依赖图:理解模块间的边界
ARCHITECTURE.md 给出了完整的依赖图,核心要点如下:
index.ts ├── Atom.ts ├── ArraySet / HistoryBuffer / capture / helpers / transactions / types ├── Computed.ts ├── ArraySet / HistoryBuffer / capture / constants / helpers / isComputed / transactions / types / warnings ├── EffectScheduler.ts ├── ArraySet / capture / constants / helpers / transactions / types ├── capture.ts ├── helpers / isComputed / types ├── isSignal.ts ├── Atom / Computed ├── localStorageAtom.ts ├── Atom / EffectScheduler └── transactions.ts ├── Atom / constants / helpers / types值得特别说明的是:Atom.ts 与 transactions.ts 相互引用,但由于二者都没有在模块求值期(module-evaluation time)使用对方,这个循环是"无害"的——这是该代码库对 JS 循环依赖问题的一种工程处理。此外capture.ts刻意绕开Computed.ts而只依赖isComputed.ts(品牌检查),同样是出于打破循环的考量。
五大架构模式
1. 统一信号接口(Signal interface pattern)
所有响应式值都实现Signal<Value, Diff>接口,对外只暴露统一的三件套get()/getDiffSince()/__unsafe__getWithoutCapture()。这让 Atom 与 Computed 可以多态混用:computed 内部、effect 内部以及 tldraw 编辑器代码里,不必关心正在读取的到底是一个可变 atom 还是一个派生 computed。
2. 依赖追踪(Dependency tracking pattern)
依赖关系由 capture.ts 管理:Child接口描述依赖者,父信号用ArraySet<Child>记录被谁依赖;通过maybeCaptureParent()在读取信号时自动登记依赖,依赖集合随每次执行动态刷新(不再读到的旧依赖会被stopCapturingParents摘除)。这意味着依赖图是"按需重建"的,永远反映最近一次执行的读取情况。
3. 惰性求值(Lazy evaluation pattern)
Computed 只有在被读取且依赖发生变化时才重算。失效判定依赖三样东西:
lastChangedEpoch:跟踪自身值何时变化;haveParentsChanged():O(parents) 的脏检查;- 一个关键捷径:正在被监听的 computed(
isActivelyListening),如果自上次检查以来没有被flushChanges遍历过、且当前没有打开的事务,就可以跳过 O(parents) 扫描直接返回缓存(Computed.ts 的注释详细解释了这一正确性前提)。
4. 事务(Transaction pattern)
支持嵌套事务与回滚:事务内 atom 的旧值记录在initialAtomValuesMap 中,abort()时逐个恢复并清空其历史缓冲;effect 的调度被推迟到事务提交之后统一进行。回滚后 effect 仍会跑一次以观察到恢复后的值(源码注释与 SPEC 中的 T7 规则有详细描述)。
5. 历史追踪(History tracking pattern)
可选的HistoryBuffer保存 diff 历史;computeDiff函数负责增量 diff 计算;无法计算 diff(或历史不足)时用RESET_VALUE表示"需要整体重置"。下游 computed / effect 通过getDiffSince()读取增量,从而从 diff 更新而不是从零重算。
性能优化清单
ARCHITECTURE.md 把优化手段分为三类,这里给出源码证据:
内存管理
ArraySet做父-子关系的高效增删与去重;EMPTY_ARRAY单例复用空数组,避免为"无 diff / 无依赖"的场景反复分配;- 历史缓冲按需惰性创建:只有传了
historyLength才在__Atom__构造函数里new HistoryBuffer(...)(Atom.ts),默认零开销。
计算效率
- epoch 失效体系:全局单一计数器,用整数比较代替深比较判断"是否变化过";
unsafe__withoutCapture提供热路径免捕获读取;singleton保证全局唯一实例(capture 栈、transaction 状态、API 版本校验等共享状态)。
调度效率
- 可插拔的
scheduleEffect:把 effect 批量延迟到 rAF 等时机执行,避免每帧多次同步重跑; deferAsyncEffects让异步流程也享受事务级批量更新;isActivelyListening状态管理:无人监听时整条依赖链停止工作,事件驱动的开销趋近于零。
按使用场景组织的 API 速查
- 基础状态管理:
atom()可变状态、computed()派生状态、react()副作用; - 高级控制:
reactor()可控反应、transact()批量更新、@computed类上派生属性; - 性能与调试:
unsafe__withoutCapture()性能优化、whyAmIRunning()依赖调试、historyLength+computeDiff+withDiff增量计算; - 类型守卫与工具:
isSignal()/isAtom()/isUninitialized()、UNINITIALIZED/RESET_VALUE常量、getComputedInstance()。
集成点:@tldraw/state 如何嵌入 tldraw 生态
外部依赖:@tldraw/utils(registerTldrawLibraryVersion()、assert())。
相关包:
- packages/state-react:React 集成层,提供
track()、useAtom、useComputed等 Hook; - packages/store:基于 @tldraw/state 的记录存储层;
- packages/editor:使用响应式状态的画布编辑器。
扩展点:
AtomOptions.isEqual:自定义相等判断,控制"什么算变化";ComputeDiff:自定义 diff 计算,控制增量更新的粒度;EffectSchedulerOptions.scheduleEffect:自定义调度策略,控制 effect 何时执行。
实战:在 tldraw 中组合使用这些原语
下面用 README.md 的完整示例串起整个响应式体系。安装(源码位于 monorepo 的 packages 目录,独立使用可发布安装):
npm install @tldraw/state最小可运行示例(atom → computed → react 全链路):
import { atom, computed, react } from '@tldraw/state' // 创建响应式状态 const name = atom('name', 'World') const count = atom('count', 0) // 自动派生值 const greeting = computed('greeting', () => { return `Hello, ${name.get()}! Count: ${count.get()}` }) // 响应变化 react('logger', () => { console.log(greeting.get()) }) // Logs: "Hello, World! Count: 0" // 更新状态 —— 反应自动执行 name.set('tldraw') // Logs: "Hello, tldraw! Count: 0" count.set(42) // Logs: "Hello, tldraw! Count: 42"批量更新消除中间态(transact 的核心价值):
import { transact } from '@tldraw/state' const x = atom('x', 0) const y = atom('y', 0) const position = computed('position', () => `(${x.get()}, ${y.get()})`) react('log-position', () => console.log(position.get())) // Logs: "(0, 0)" transact(() => { x.set(10) y.set(20) // 事务内 effect 不会执行,提交后统一跑一次 }) // Logs: "(10, 20)"历史与增量 diff(基于 HistoryBuffer + RESET_VALUE):
const canvas = atom( 'canvas', { shapes: [] }, { historyLength: 100, // 最多保留 100 条 diff computeDiff: (prev, next) => ({ prev, next }), } ) // 记住当前位置 const startEpoch = canvas.lastChangedEpoch // ...做一些修改... canvas.update((state) => ({ shapes: [...state.shapes, newShape] })) // 获取自该 epoch 以来的 diff;历史不足时返回 RESET_VALUE const diffs = canvas.getDiffSince(startEpoch)性能优化(读值但不建立依赖):
import { unsafe__withoutCapture } from '@tldraw/state' const expensiveComputed = computed('expensive', () => { const important = importantValue.get() // 读取该值但不使其成为依赖 const metadata = unsafe__withoutCapture(() => metadataAtom.get()) return computeExpensiveValue(important, metadata) })调试(找出"为什么又跑了"):
import { whyAmIRunning } from '@tldraw/state' react('debug-reaction', () => { whyAmIRunning() // 打印触发本次执行的依赖树 // 你的反应代码... })接入 tldraw 编辑器(在onMount回调中):
const selectedShapes = computed('selectedShapes', () => { return editor.getSelectedShapeIds().map((id) => editor.getShape(id)) }) // 响应选区变化,调用返回的函数即可停止 const stop = react('update-property-panel', () => { const shapes = selectedShapes.get() updatePropertyPanel(shapes) })接入 React(需安装绑定包):
npm install @tldraw/state-reactimport { track, useAtom, useComputed } from '@tldraw/state-react' // track() 让组件在读取到的任何信号变化时自动重渲染 const Counter = track(function Counter() { const count = useAtom('count', 0) const doubled = useComputed('doubled', () => count.get() * 2, [count]) return ( <div> <p>Count: {count.get()}</p> <p>Doubled: {doubled.get()}</p> <button onClick={() => count.set(count.get() + 1)}>+</button> </div> ) })给 AI Agent 的源码导航建议
ARCHITECTURE.md 最后给出了面向 Agent 的阅读路径,正好可以作为理解整个包的"最小学习路线":
- 理解信号:先读 types.ts → Atom.ts → Computed.ts;
- 理解响应式:先读 EffectScheduler.ts → capture.ts;
- 理解事务:直接读 transactions.ts;
- 理解公开 API:看 index.ts 的全部导出;
- 理解性能:聚焦 helpers.ts → ArraySet.ts;
- 理解变更追踪:聚焦 HistoryBuffer.ts。
按功能定位关键入口:
- 创建状态:Atom.ts 的
atom() - 派生值:Computed.ts 的
computed() - 副作用:EffectScheduler.ts 的
react() - 批量更新:transactions.ts 的
transact() - 调试:capture.ts 的
whyAmIRunning()
如果需要更细的行为约束,SPEC.md 提供了带规则 ID 的行为规范(测试用例会引用其中的规则编号),是理解边界行为的权威参考;面向人类的完整 API 文档则在 DOCS.md 中。
【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考