markstream-vue 源码阅读笔记

maokaihe maokaihe #markstream-vue #Vue #Source Code Reading

想在 LLM 时代读一读 markstream-vue 的源码,看看流式 markdown 渲染在前面那段是怎么设计的。

7月份就想做一个markstream-vue的源码阅读,因为偶然在推上刷到了,正好现在LLM流行的时代,软件都用了类似的相关技术,于是正好想学习一下里面的设计原理,借助AI的帮助下可以让我在源码里面更好阅读。然后后来一直在做别的事情,还有学校的事情忙着,没什么时间静下心来读代码。今天是8月21号了,好不容易闲下来做点自己想学的,于是借助AI先帮我总结一下我应该从哪里开始看。

这个整个流程的大致逻辑就是: 从LLM的SSE传过来的chunk,处理得到markdown字符串,然后对string进行parsing,得到一个具体的Node,然后NodeRender把Node渲染成DOM。 然后大致里面的细节有smoothStreaming,有一个当前source(需要渲染的)和visible(实际展现的)的概念;以及统计渲染耗时,动态的调整batchsize的大小。对于一节涉及到是否闭合的markdown来说,不能拿过来就立刻渲染,需要设计延迟等待等等。 还有如何减少内存分配,减少内存中JS Object的数量,把新的东西append到已经有的Object里,减少开销。

大致流程就是这样,我跟着ai总结看的,现在我会开始看源码了,我每天看一点就会写点自己从中学到的设计思想或者单纯的代码技巧,还有我没了解过的vue的知识点。希望能在两个月写完吧,因为快要开学了,不一定又有什么乱七八糟的事情折磨人了。

markstream-vue 里的 Effect Scope 和 Performance API

maokaihe maokaihe #markstream-vue #Vue #Source Code Reading

读源码时遇到的两个 API:用 onScopeDispose 回收副作用,以及用 performance.mark 和 measure 统计耗时。

读 markstream-vue 源码时遇到了两个之前不太熟的 API,顺手记一下。

用 Effect Scope 清理副作用

Vue 3.2 加入了 Effect Scope,可以把 watch、computed 这类响应式副作用收进同一个作用域,再一次性清理。

组件本身已经有一个 Effect Scope,组件卸载时,Vue 会自动停止里面的响应式副作用。不过 composable 里可能还会创建订阅、控制器之类的资源,它们也需要跟着组件一起销毁。源码里用了下面这种写法:

if (getCurrentScope()) {
onScopeDispose(() => {
unsubscribe()
controller.destroy()
})
}

getCurrentScope() 用来检查当前有没有活跃的 Effect Scope。比如这段代码在组件的 setup() 中执行,就能拿到组件的作用域。

onScopeDispose() 则把清理函数注册到当前作用域。等组件卸载或这个作用域被手动停止时,unsubscribe() 和 controller.destroy() 会一起执行。它有点像 onUnmounted(),但不依赖组件,可以直接用在 composable 里。

这段判断也很实在:有 Vue 作用域时就自动注册清理;不在任何作用域里时不注册,资源要由调用方自己处理。

用 Performance API 统计耗时

我一开始把 performance 记成了 Node 内置 API。更准确地说,它来自 Web Performance API,浏览器可以直接使用;Node 也通过 node:perf_hooks 提供了兼容实现。

performance.now()

performance.now() 返回从 performance.timeOrigin 开始经过的毫秒数,适合测量一段代码花了多久。它不是当前时间戳。

const start = performance.now()
await parseMarkdown(markdown)
const duration = performance.now() - start

performance.mark()

mark() 会在性能时间线上记一个带名字的时间点:

performance.mark("parse-start")
await parseMarkdown(markdown)
performance.mark("parse-end")

每次调用都会生成一个 PerformanceMark,里面有 name、startTime、entryType 和 detail 等字段。

performance.measure()

measure() 用来计算两个 mark 之间的耗时:

const measure = performance.measure(
"parse-cost",
"parse-start",
"parse-end",
)
console.log(measure.duration)

也可以直接传开始和结束时间:

const measure = performance.measure("manual", {
start: 100,
end: 350,
})
console.log(measure.duration) // 250

相比自己到处保存开始时间,mark() 和 measure() 会把结果留在性能时间线上,调试时更容易统一查看。