4.2 内部架构剖析:Owner树、响应式图与编译器


4.2 内部架构剖析:Owner 树、响应式图与编译器

本节摘要:Solid 的运行时由三层协作:编译器把 JSX 切成静态模板与动态绑定;响应式内核维护一张信号与计算节点构成的依赖图;运行时负责挂载、调度与销毁。本节给出分层架构图,初见 Owner、订阅、调度三个内核概念,并演示组件之外的世界——createRootbatch

某个深秋的调试夜,一位迁移者盯着屏幕发问:组件函数明明已经执行完了,为什么按钮点击后界面还是会变?回答这个问题需要把 Solid 的盒子打开——组件函数只是装配工,装配完成后,真正常驻工作的是另外两样东西:一张依赖图和一套调度机制。本节就沿着这张分层图,把常驻的部分看清楚。

三层架构:各自管什么

图:Solid 架构分层——编译器、内核、运行时

图:Solid 架构分层——编译器、内核、运行时

分层读法自上而下:编译层决定"静态与动态的边界在哪",这道边界画得越准,运行时要做的事越少;内核是常驻的账本,记着谁订阅谁、值是多少、谁该重算;运行时是执行队,负责把账本上的变动落到真实 DOM。React 的分层里没有"编译层切分动静"这一层——它的编译只做语法转换,动静边界完全交给运行时对账去试。

内核三概念:Owner、订阅、调度

Owner(作用域归属者)。每个响应式实体——信号、Memo、Effect、控制流分支——创建时都会登记"我属于哪个作用域"。组件是一个作用域,Effect 是其内的子作用域,Show 的每个分支各自是子作用域。作用域销毁时,名下所有实体按序清理:信号退订、Effect 停止、DOM 节点摘除。第 3 章讲的"清理跟着作用域走",底座就是这张 Owner 树。它回答的是"谁负责收拾谁"。

订阅(依赖边)。读取现场登记的订阅关系构成一张有向图:信号节点指向订阅它的计算节点。这张图是运行的地图——更新投放走这张图,内存回收也看这张图(图上不可达的节点随 Owner 一起回收)。它回答的是"变化流向哪里"。

调度(批量冲刷)。信号写入并不立即执行下游,而是把受影响的计算节点排进微任务队列,同一轮同步代码里的一串写入合并为一次冲刷。它回答的是"什么时候执行"。

import { createSignal, batch } from "solid-js"; const [firstName, setFirstName] = createSignal("三"); const [lastName, setLastName] = createSignal("张"); // 不用 batch:两次写入,下游绑定本可能跑两轮(调度会合并相邻写入, // 但跨异步边界的多次写入就需要显式 batch) batch(() => { setFirstName("四"); setLastName("李"); }); // 下游只在这轮同步代码结束后冲刷一次

batch 的价值在异步边界处最明显:await 前后各写一个信号,默认会被调度拆成两轮冲刷,包进 batch 就并成一轮。React 18 的自动批处理覆盖了大多数场景,但跨 Promise、setTimeout 的写入仍需要 flushSync 之类的手动干预——两边都存在"异步边界批处理"这个课题,只是 API 形态不同。

组件之外的世界:createRoot

组件里的响应式实体自动挂在组件作用域下;但有些代码天生活在组件外——全局主题管理器、路由器、第三方桥接层。它们创建的 Effect 与信号需要一个"人工 Owner",这就是 createRoot

import { createRoot, createSignal, createEffect } from "solid-js"; // 模块级全局状态:自带一个长期存活的根作用域 const [theme, setTheme] = createSignal("light"); createRoot(dispose => { createEffect(() => { document.body.dataset.theme = theme(); }); // dispose 是这个根的手动销毁开关,测试里常用来清理 });

React 里组件外写响应逻辑基本等于引旁路(外部 store 加订阅器),Solid 里 createRoot 是官方通道。这个差异在第 5 章的全局状态方案、第 9 章的测试写法里都会反复用到。

💡 阅读源码或深入调试时,把"组件树"与"Owner 树"分开画:前者是装配关系,执行完就退场;后者是归属关系,常驻到销毁。混淆这两棵树是理解 Solid 内部行为时最常见的弯路。

异步边界:事件回调与 runWithOwner

createRoot 之外还有一个更容易踩到的"脱离作用域"场景:事件回调与定时器。这些代码执行时,追踪指针早已归位(组件函数执行完了),在其中读取信号不会登记依赖——这通常是正确行为(回调要的是最新值,不是订阅)。但如果回调里创建响应式实体(比如按需 new 一个 Effect),它就成了孤儿:没有 Owner,销毁时无人回收。规范做法是在同步代码里预先建好需要的实体,回调只负责写信号;确需动态创建时,用 Owner 工具显式指定归属。React 没有这层心智,因为它的模型里"在回调里设置状态"天然合法——状态本来就在组件实例上挂着。

问题:Solid 为什么不需要可中断调度?

React 的并发渲染能把对账任务切片、让位给更高优先级的输入——因为它的更新是一大块可分片的工作。Solid 的更新天然就是最小粒度:一次投放通常只是写入几个节点,没有"切片"的必要;长任务在 Solid 应用里的常见来源是业务计算而非框架流程,那该用 Web Worker 或时间分片在业务层解决。这个对照常被误读为"谁更先进",公平的说法是:调度器的复杂度与更新块的体积成正比,更新块小到极致,调度器就可以薄到近乎透明。

本节要点回顾

  • 三层分工:编译层画动静边界,内核记账(依赖图与 Owner 树),运行时装配与调度。
  • Owner 管回收:作用域销毁即名下实体全清,清理语义的地基。
  • 订阅管流向:依赖边是更新投放的地图,也是内存回收的依据。
  • createRoot 是组件外的合法通道:全局逻辑不再需要旁路。

内核的图画好了,下一章回到书写层最频繁的操作——条件与列表,看控制流组件如何把这张图用足。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U