资讯详情

ShareDB 客户端 Doc 类完整指南:文档属性、操作方法、事件与底层实现

📅 2026/10/7 9:31:55 | 华诺云谱 👁 阅读
ShareDB 客户端 Doc 类完整指南:文档属性、操作方法、事件与底层实现
后端数据库【免费下载链接】sharedbRealtime database backend based on Operational Transformation (OT)项目地址https://gitcode.com/gh_mirrors/sh/sharedb点击查看免费下载ShareDB 的Doc类lib/client/doc.js是客户端对数据库中一份共享文档的本地视图与操作入口它承载了基于 Operational TransformationOT的取数、订阅、提交操作、删除与事件监听等全部客户端能力。本文以 docs/api/doc.md 为骨架结合仓库源码与测试逐项拆解Doc的属性、方法、事件及底层机制读完你可以直接基于Doc编写可运行的前端协同逻辑。Doc 是什么Doc是 ShareDB 文档在客户端侧的表示客户端对 sharejs 文档的一个视图。它由collection集合名与id文档 ID唯一标识负责从服务器拉取文档快照fetch/subscribe在本地应用 OT 操作并同步到服务器submitOp/create/del通过事件机制让 UI 感知本地与远端变更op、load、create、del等管理订阅状态、待发送操作队列与连接重连后的恢复。获取 Doc 实例Doc不应该直接new创建而是通过connection.get(collection, id)获取var connection new sharedb.Connection(socket); var doc connection.get(examples, counter);对应 API 见 docs/api/connection.md。需要注意同一个Connection上对同一collectionid重复调用get()会返回同一个Doc实例——lib/client/connection.js 中Connection.prototype.get先在this.collections[collection][id]缓存里查找找不到才新建Doc并发出doc事件。这一点在 test/client/doc.js 中被专门测试it(getting twice returns the same doc, function() { var doc this.connection.get(dogs, fido); var doc2 this.connection.get(dogs, fido); expect(doc).equal(doc2); });因此如果你在多个模块里反复get同一个文档得到的是同一对象事件监听与状态天然共享。Properties属性type-- Type文档的 OT 类型。如果文档已被fetch/subscribe过但type仍为空说明该文档尚未被创建。源码中 lib/client/doc.js 的_setType负责设置类型传入字符串时先通过types.map[newType]查表传入null时同时清空data。collection-- string 与id-- string文档所属集合名与全局唯一文档 ID。二者在Doc构造时lib/client/doc.js从connection.get()传入并固定此后所有网络消息都以c、d字段携带这对标识。version-- number从服务器获取到的最新文档版本号。随着服务器发来 opversion会自增。有一个易被忽略的细节本地 op 通过submitOp()提交后version只在服务器确认即submitOp回调被调用后才会递增。对应实现见 lib/client/doc.js 的_opAcknowledged收到确认后this.version。data-- Object文档内容。只有 fetch 或 subscribe 之后才可用未获取时为undefined。Doc内部通过_setDatalib/client/doc.js统一更新data并递增_dataStateVersion计数器该计数器用于廉价地判断数据是否变化。preventCompose-- boolean默认false。设为true可以阻止 op 被合并compose成单个 op 提交。该值是在调用submitOp()的当时被读取的所以可以针对某个具体 op 临时开启、提交后再关闭。底层逻辑在_tryComposelib/client/doc.js第一行即if (this.preventCompose) return;。关于 compose 的语义两个连续 op 如果类型支持compose会被合并为一个 op 发送减少网络往返。submitSource-- boolean默认false。设为true时op 的source会随 op 一起发送到服务器。开启后只有source相同的 op 才会在本地被 compose 合并——这正是 lib/client/doc.js 中if (this.submitSource !deepEqual(op.source, last.source)) return;的用途source不同则放弃合并保证每个来源的 op 都能被正确区分与追踪。Methods方法fetch()用服务器上的文档快照填充datadoc.fetch([callback])callback可选签名function(error) { ... }快照取回后调用。从源码看fetch走_fetchlib/client/doc.js把回调压入pendingFetch若当前无 inflight fetch 则发出一条 fetch 请求。多个 fetch 若在极短时间内连发只会有一次真正发出if (!shouldSend) return;其余回调排队等同一份快照这是天然的防抖。对应测试见 test/client/doc.js「only fetches once when calling in quick succession」。subscribe()用服务器快照填充data同时持续监听服务器端该文档的变更并在后续变更时触发op事件doc.subscribe([callback])callback可选签名function(error) { ... }初始快照取回后调用。订阅与取消订阅共用内部_queueSubscribelib/client/doc.js若相邻请求的wantSubscribe相同会被合并为一个请求回调被combineCallbacks合并执行。unsubscribe()停止接收该文档的更新。取消订阅时文档数据仍保留在内存中只是不再与服务器保持同步。之后可以再次subscribe()重新订阅doc.unsubscribe([callback])callback可选签名function(error) { ... }取消订阅完成后调用。注意 lib/client/doc.js 中 unsubscribe 请求一旦发出subscribed就被保守地立即置为false不需要等服务器回执。toSnapshot()为当前文档生成一份 Snapshot 数据可序列化为 JSON、反序列化后传给Doc.ingestSnapshot从而在不经过 ShareDB 连接的情况下初始化一个客户端 Docconst snapshot doc.toSnapshot()实现见 lib/client/doc.jsDoc.prototype.toSnapshot function() { return { v: this.version, data: clone(this.data), type: this.type.uri }; };注意data做了深拷贝clone防止外部修改doc.data反向污染快照——test/client/doc.js 专门验证了这一点。ingestSnapshot()把 Snapshot 数据注入当前 Docdoc.ingestSnapshot(snapshot [, callback])警告该方法一般由fetch()/subscribe()内部调用不建议业务代码直接调用只有在需要把连接之外传输过来的快照灌入 Doc 时才使用。传入的 snapshot必须包含data、v版本号和type三个属性。底层校验逻辑在 lib/client/doc.js缺少数字类型v时报ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION若文档已创建或有待提交 op且本地version为null则报ERR_DOC_MISSING_VERSION若快照版本高于本地版本则自动发起一次fetch()追赶若本地版本已高于快照版本则直接忽略任何情况下都不会把版本往回拨快照的type未指定时使用默认类型且若类型实现了deserialize会先反序列化数据最后发出load事件。destroy()取消订阅并停止触发事件同时从所属Connection中移除该文档引用使Doc可以被垃圾回收doc.destroy([callback])callback可选签名function(error) { ... }文档销毁完成后调用。实现见 lib/client/doc.js置_wantsDestroy true待whenNothingPending后视订阅状态先unsubscribe再调connection._destroyDoc(doc)lib/client/connection.js 中_destroyDoc会从collections缓存中摘除该文档并发出destroy事件。之后再次connection.get()会得到全新的Doc 实例见 test/client/doc.js。create()在本地创建文档并把 create 操作发送到服务器doc.create(data [, type [, options [, callback]]])data文档内容结构取决于文档 typetype文档的 type缺省时使用默认类型源码中为types.defaultType.urioptions可选默认{}options.source任意值默认true该值会传给本地事件处理器以区分 op 的产生方式只有submitSource为true时才会随 op 发往服务器callback可选function(error) { ... }文档被服务器提交commit后调用。实现要点lib/client/doc.js若this.type已存在直接以ERR_DOC_ALREADY_CREATED拒绝否则构造{create: {type, data}}交给_submit。在 ShareJS 语义中create 即“设置文档的类型”对象在数据库中隐式存在只是没有数据与类型。submitOp()在本地应用一个操作并发送到服务器。调用前文档必须已 fetch 或 subscribedoc.submitOp(op [, options [, callback]])op要提交的操作其结构取决于文档 type如 json0 的[{p: [numClicks], na: 1}]options可选默认{}options.source同createcallback可选function(error) { ... }op 被服务器提交后调用。典型用法见 examples/counter/client.js点击按钮时doc.submitOp([{p: [numClicks], na: 1}])本地立即生效随后同步到服务器与其他客户端。底层_submitlib/client/doc.js会先_pushOp入队、立即_otApply本地应用再用nextTick延迟 flush使同步连续多次 submit 的 op 能在发送前被 compose 合并减少网络请求。del()在本地删除文档并向服务器发送删除操作doc.del([options [, callback]])重要提示ShareDB 的文档及其操作历史永远不会被真正删除删除只是把文档tombstoned墓碑化——文档仍存在版本号继续保留删除后版本 1。options与callback语义同submitOp。实现见 lib/client/doc.js若文档无类型未创建报ERR_DOC_DOES_NOT_EXIST否则构造{del: true}提交。本地执行时_setType(null)清空类型与数据并发出del事件带删除前的旧数据。whenNothingPending()在以下条件全部满足后调用回调所有由submitOp()提交的 op 都已发送到服务器且所有挂起的fetch()、subscribe()、unsubscribe()请求都已被处理。doc.whenNothingPending(callback)警告它不会等待挂起的model.query()查询。callback签名function(error) { ... }。实现依据是hasPendinglib/client/doc.js——检查inflightOp、pendingOps、inflightFetch、inflightSubscribe、pendingFetch、pendingSubscribe是否为空空则下一事件循环直接回调非空则注册到内部nothing pending事件上。pause() / resume()pause()阻止本地 op 发送到服务器若已订阅远端 op 仍然正常接收lib/client/doc.js置paused trueresume()解除暂停并立即 flush 排队的 oplib/client/doc.js置paused false后调用this.flush()。flushlib/client/doc.js的核心约束是同一时刻只有一个 op 在途if (!this.connection.canSend || this.inflightOp) return;未暂停且有待发 op 时才发下一个。Events事件所有事件的source参数遵循统一规则远端 op来自其他客户端为false本地提交的 op 为 truthy——本地 op 时其值为submitOp传入的source未提供则为true。loadingestSnapshot加载了一份文档快照fetch/subscribe都会触发doc.on(load, function(source) { ... })警告ShareDB 的错误恢复机制可能在出错时触发 “hard rollback”此时Doc会自动调用fetch()因此要把该事件的处理与初始subscribe()回调分开处理不要假设 load 只发生一次。create文档被创建此时Doc将拥有typedoc.on(create, function(source) { ... })sourceboolean | any规则同上。底层在_otApply的op.create分支发出lib/client/doc.js。before op一个操作即将应用到datadoc.on(before op, function(op, source) { ... })op即将应用到文档的操作sourceboolean | any。这个事件很适合在数据变更前读取旧值源码注释明确说明其用途是让客户端在快照被改之前取出所需数据。测试还验证了在before op处理器内同步提交的 op 会被 transform 以保持一致性test/client/doc.js。op一个操作已应用到数据doc.on(op, function(op, source) { ... })与op batch的区别对于 json0多组件 op 会被拆分shatter成单个组件逐一触发。例如[{p: [list, 0], li: a}, {p: [list, 1], li: b}]会拆成[{p: [list, 0], li: a}]与[{p: [list, 1], li: b}]两个组件op对每个组件触发一次而op batch只触发一次。这是为了便于绑定层如 DOM 绑定把 op 事件逐个翻译为对应的 UI 变更且保证触发时快照只包含当前组件的更新。相关逻辑见 lib/client/doc.js 的多组件增量应用分支。UI 绑定的典型用法见 examples/counter/client.jsdoc.on(op, showNumbers)在每次变更后刷新页面。before op batch一个可能包含多个组件的操作即将应用到datadoc.on(before op batch, function(op, source) { ... })op batch一个可能包含多个组件的操作已应用到datadoc.on(op batch, function(op, source) { ... })op为该批操作的完整数组source规则同上。本地多组件 op 的 batch 事件行为见 test/client/doc.js。del文档被删除doc.on(del, function(data, source) { ... })data文档被删除之前的datasourceboolean | any。实现见 lib/client/doc.js先保存oldData this.data_setType(null)清空类型与数据再发出del事件。error发生错误。通常是因为某个异步函数被调用时没有传入回调错误无处投递而改为抛出事件doc.on(error, function(error) { ... })error为 ShareDBError。源码中大量分支遵循同一模式if (callback) return callback(err); return this.emit(error, err);——有回调走回调没有回调就发事件避免错误被吞掉。深入底层一个 op 的完整生命周期结合以上 API一次submitOp的完整链路如下对应源码均在 lib/client/doc.jssubmitOp构造{op: component}并调用_submitL871-L879_submit校验文档已创建否则报ERR_DOC_DOES_NOT_EXIST、必要时用type.normalize规范化 op然后_pushOp入队、_otApply本地应用L744-L783_pushOp尝试把 op compose 到队尾最后一条待发 op 上受preventCompose、submitSource约束失败则 push 进pendingOpsL785-L805nextTick触发flush把队首 op 置为inflightOp并发送op 上打上src连接 ID与自增seq作为唯一标识用于断线重连后的去重与确认匹配L687-L731服务器回包后_handleOp确认对在途 op 做版本核对、处理服务端 transform 后的fixup最终version并回调L329-L391、L958-L991若服务器拒绝op能 invert 的类型先本地反转回滚_rollbackL993-L1038无法反转或应用出错则hard rollback——清空类型、版本、队列强制fetch服务器最新状态恢复_hardRollbackL1040-L1105。这正是load事件可能在非订阅回调场景下再次触发的原因。测试 test/client/doc.js 覆盖了 invalid op 回滚、不可逆 op 回滚、两客户端并发冲突恢复rescues an irreversible op collision等边界场景可以作为理解这些机制的活教材。小结Doc是 ShareDB 客户端一切功能的落点fetch/subscribe解决“取数与实时同步”submitOp/create/del解决“写入与 OT 协同”成对的事件before op/op、before op batch/op batch服务于不同粒度的 UI 绑定需求而toSnapshot/ingestSnapshot则支持跨连接初始化。理解Doc的属性、方法、事件与底层回滚机制是正确编写 ShareDB 前端应用、排查同步异常的前提。继续阅读 docs/api/connection.md 了解Doc的获取来源或 docs/types/json0.md 掌握默认类型的 op 写法。赞分享后端数据库【免费下载链接】sharedbRealtime database backend based on Operational Transformation (OT)项目地址https://gitcode.com/gh_mirrors/sh/sharedb点击查看免费下载相关推荐Calibre 新手教程5 分钟跑通 PDF 到 EPUB 的第一次格式转换Calibre 新手教程5 分钟跑通 PDF 到 EPUB 的第一次格式转换 手机里存了几百本固定版式的电子书窄屏上只能不停缩放、滑动Calibre 是一后端数据库Prompt Engineering Guide3步本地跑起一套完整的大模型提示词工程教程Prompt Engineering Guide3步本地跑起一套完整的大模型提示词工程教程 Prompt Engineering Guide 是一个开源的提示后端数据库Mineflayer 4.x 完整 API 指南从 createBot 到事件、属性与底层方法的全量实战手册Mineflayer 4.x 完整 API 指南从 createBot 到事件、属性与底层方法的全量实战手册 本篇指南以官方俄文版 API 文档 docs/游戏开发上一篇Semi Design OverflowList 折叠列表组件全解析collapse 与 scroll 双模式实现自适应溢出折叠下一篇SQL Assessment API External Probe 深度解析用任意 .NET 代码扩展 SQL Server 评估创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑