
Ember.js 内部架构解析Glimmer Runtime 的 Reference 抽象与路径引用系统【免费下载链接】ember.jsEmber.js - A JavaScript framework for creating ambitious web applications项目地址: https://gitcode.com/gh_mirrors/em/ember.jsGlimmer 是 Ember.js 的高性能渲染引擎其运行时Runtime的核心抽象是一个名为Reference的稳定对象类型它表示一个纯无副作用计算的结果且该结果会随时间变化。本文以仓库内部文档 internal-docs/guides/04-references.md 为骨架结合glimmer/reference、glimmer/interfaces与glimmer/runtime的真实实现系统讲解 Reference 的接口设计、组合与组合子、惰性求值、路径引用PathReference扩展以及它们在模板渲染中的实际作用。读完本文你将理解 Glimmer拉取式pull-based响应系统的底层原理并能读懂参考链reference chain从模板编译到 DOM 更新的完整工作方式。前置背景Glimmer Runtime 中的三大概念在深入 Reference 之前先回顾 运行时总览 中对 Glimmer 运行时的定位编译后的字节码在浏览器中被渲染为组件树同时 Glimmer 会为组件、helper 与值建立一棵references 与 validators 树用于高效判断组件层级中某棵子树的状态是否变化、是否需要重渲染。运行时的核心围绕三个概念展开Components可复用的 UI 单元其确切语义由宿主环境如 Ember通过 component manager 决定References代表纯计算结果、可随时间变化的稳定对象用于在模板中共享与高效更新值Validators提供计算结果新鲜度freshness保证的稳定对象可组合——任一子 validator 变化父 validator 也会被标记为变化。本文的主角是第二个概念Reference。它解决的核心问题是——如何把一个会随时间变化的计算包装成一个稳定、可传递、可组合的一等公民对象。Reference一个只有value()的抽象Glimmer 运行时的核心原语是Reference抽象数据类型。本质上它是一个稳定对象代表某个纯无副作用计算的结果而这个结果可能随时间改变。其接口极其精简interface ReferenceT { value(): T; }如果你熟悉 FRP函数式响应式编程术语可以把它理解为离散的信号signal。但它与 Ember 传统的Stream、ReactiveX 的Observable等构造有一个关键区别Reference 是纯拉取式pull-based系统没有订阅subscriptions或通知notifications的概念。正如前一篇预编译器总览与运行时总览所述Glimmer 团队认为拉取式系统更适合这类渲染问题也最终更高效下一章 Validators 将讨论一种无需通知即可追踪变化的技术。最小示例捕获变量的当前值下面构造一个简单的 Reference它在整个生命周期内捕获变量foo的值let foo 1; let fooReference: Referencenumber { value() { return foo; } }; fooReference.value(); // 1 foo; fooReference.value(); // 2如你所见每次调用fooReference.value()都会返回foo变量当前的值。这个例子虽简单却点出了 Reference 抽象的力量JavaScript 变量本身总是持有值值可以被传递给其他函数、被其他函数持有但变量绑定binding本身并不是一等公民。借助 Reference 系统我们可以轻松地按引用by reference传递变量——这正是Reference数据类型名字的由来。组合Composition把计算组装成图Reference 天然可组合。下面用 Reference 建模foo bar计算let foo 1; let bar 2; let fooReference: Referencenumber { value() { return foo; } }; let barReference: Referencenumber { value() { return bar; } }; let fooPlusBarReference: Referencenumber { value() { return fooReference.value() barReference.value(); } }; fooPlusBarReference.value(); // 3 foo 2; fooPlusBarReference.value(); // 4 bar 3; fooPlusBarReference.value(); // 5可以看到fooPlusBarReference组合了fooReference与barReference而不是直接访问变量。当foo、bar随时间变化时fooPlusBarReference始终返回正确的foo bar结果。这构成了一个引用链reference chain叶子是原始值引用内部节点是派生计算——这正是 Glimmer 渲染时构建的数据结构的缩影。组合子Combinators把常见操作泛化正因为 Reference 高度可组合很容易写出高阶组合子来建模常见操作。例如把fooPlusBarReference泛化成一个可复用的AdditionReference类class AdditionReference implements Referencenumber { private lhs: Referencenumber; private rhs: Referencenumber; constructor(lhs: Referencenumber, rhs: Referencenumber) { this.lhs lhs; this.rhs rhs; } value(): number { return this.lhs.value() this.rhs.value(); } }另一个经典例子是map操作——把一个 Reference 的值通过映射函数转换成新值// 一个 Mapper 是接受类型 T 的值、返回类型 U 的新值的函数 type MapperT,U (T) U; function mapT,U(source: ReferenceT, mapper: MapperT,U): ReferenceU { return new MapperReference(source, mapper); } class MapperReferenceT, U implements ReferenceU { private source: ReferenceT; private mapper: MapperT,U; constructor(source: ReferenceT, mapper: MapperT,U) { this.source source; this.mapper mapper; } value(): U { let { source, mapper } this; return mapper(source.value()); } } let foo 4919; let fooReference: Referencenumber { value() { return foo; } }; // 将数字转换为十六进制base 16表示 let toHexMapper: Mappernumber, string function(num) { return 0x num.toString(16).toUpperCase(); }; let hexReference map(fooReference, toHexMapper); hexReference.value(); // 0x1337 foo 49374; hexReference.value(); // 0xC0DE这些组合子与后续 Validators 一章中的ConcatReference、UppercaseReference在思路上完全一致每个组合子只关心自己的直接输入 Reference通过递归地拉取输入值来计算自身结果。惰性求值Lazy Evaluation按需才调用value()由于 Reference 是拉取式的实现惰性求值语义非常简单——只要在必要时才调用.value()。考虑一个建模 JavaScript 三元表达式condition ? ifTrue : ifFalse的朴素实现class ConditionalExpressionReferenceT implements ReferenceT { private predicate: Referenceboolean; private consequent: ReferenceT; private alternative: ReferenceT; constructor(predicate: Referenceboolean, consequent: ReferenceT, alternative: ReferenceT) { this.predicate predicate; this.consequent consequent; this.alternative alternative; } value(): T { let predicate this.predicate.value(); let consequent this.consequent.value(); let alternative this.alternative.value(); return predicate ? consequent : alternative; } } let dayOfWeek Friday; let isWorkDay: Referenceboolean { value() { return dayOfWeek ! Saturday dayOfWeek ! Sunday; } }; let work: Referencestring { value() { let result []; result.push(Working...); result.push(Working...); result.push(Working...); result.push((X_X)); return result.join( ); } }; let relax: Referencestring { value() { return Relaxing... (v_v) } }; let result new ConditionalExpressionReference(isWorkDay, work, relax); result.value(); // Working... Working... Working... (X_X) dayOfWeek Saturday; result.value(); // Relaxing... (v_v)这个实现虽然可用但会急切地同时求值consequent与alternative两个引用——即使最终只会用到其中一个值。这并不理想因为引用可能代表任意昂贵的计算。改进为惰性求值此处即短路求值class ConditionalExpressionReferenceT implements ReferenceT { // ... value(): T { let { predicate, consequent, alternative } this; if (predicate.value()) { return consequent.value(); } else { return alternative.value(); } } }改进后可以保证两个分支中只有一个被求值消除了可能昂贵且浪费的计算。这一按需拉取的设计原则贯穿整个 Glimmer 运行时——后面会看到HashReference利用同样的思想避免无谓求值。References in Glimmer模板动态段如何工作Reference 在 Glimmer 模板系统中扮演着极其重要的角色。当 Glimmer 渲染一个模板时每个动态段dynamic segment——例如b{{foo}}/b中的{{foo}}——都由一个 Reference 表示。初次渲染时通过从这些引用中拉取初始value()来填充动态段之后模板可以随时用最新数据重渲染只需从每个引用中拉取最新的value()并相应更新 DOM 节点后半部分的机制将在后续章节讨论。Reference 还承担着桥接系统不纯有副作用部分与纯函数式部分的重任。上下文context / self查找在 Handlebars 中模板总是针对一个上下文在 Glimmer 内部常称为self渲染类似 JavaScript 调用函数时的this。以如下模板为例h1Welcome, {{user.name.first}}!/h1 pMessage of the day: {{motd}}/p假设motd不是 helper那么两个动态段描述的都是对上下文的路径查找Glimmer 内部称 self lookup。也就是说{{user.name.first}}引用的是this.user.name.first的值其中this就是上下文对象。事实上为清晰起见它们可以改写为{{this.user.name.first}}与{{this.motd}}。由于上下文可能在两次重渲染之间从一个对象变成另一个对象上下文本身也被建模为一个 Reference。而 Handlebars 支持对上下文做任意路径查找如上面的user.name.first因此 Glimmer 需要一种能力从上下文 Reference 出发为给定路径创建子引用。一个可行的方案如下// 编码了 Handlebars 中 软失败 的路径查找语义 // // 用法 // let obj { foo: { bar: baz } }; // get(obj, foo) { bar: baz } // get(obj, foo, bar) baz // get(obj, foo, nope) undefined // get(obj, foo, bar, baz) undefined function get(object: any, ...subpaths: string[]) { if (subpaths.length 0) { return object; } if (object typeof object object) { let head subpaths[0]; let tail subpaths.slice(1); return get(object[head], ...tail); } } class PathLookupReference implements Referenceany { private context: Referenceany; private subpaths: string[]; constructor(context: Referenceany, path: string) { this.context context; this.subpaths path.split(.); } value(): any { return get(this.context.value(), ...this.subpaths); } } let context { user: { name: { first: Godfrey, last: Chan } }, motd: Welcome back! } let contextReference: Referenceany { value() { return context; } }; // {{user.name.first}} let firstName new PathLookupReference(contextReference, user.name.first); // {{motd}} let motd new PathLookupReference(contextReference, motd); firstName.value(); // Godfrey motd.value(); // Welcome back! context.user.name { first: Yehuda, last: Katz }; firstName.value(); // Yehuda注意get的软失败语义路径中任何一环缺失都返回undefined而不是抛错——这保证了{{user.address.zip}}之类的模板在数据不完整时依然能安全渲染为空。为什么需要PathReference扩展上面的实现虽然可用但因为它在求值时把上下文 Reference求值成普通值父引用与子引用之间并没有建立有意义的连接。而实际上上下文对象上有时会带有一些我们希望向下游引用传播的额外信息上下文可能只是字符串、数字、undefined之类的原始类型此时后续所有路径查找都会得到undefined上下文可能是不可变数据结构此时只要上下文对象本身没有被替换下游的所有value()都无需重算Handlebars 的某些高级特性以及 Ember 等宿主环境的扩展意味着几乎任何引用如 helper 的返回值都可能出现在路径查找的位置。出于这些原因Glimmer 在基础Reference之上定义了一个扩展类型PathReferenceinterface PathReferenceT extends ReferenceT { get(path: string): PathReferenceany; }除了value()方法外PathReference还支持get方法负责把这些路径查找转换为子引用。这允许父引用向下编码并传播额外信息。示例一原始值引用PrimitiveReference原始值字符串、数字、undefined等上所有后续路径查找永远返回undefined因此可以返回一个常量、专门的PathReferenceconst NULL_REFERENCE: PathReferencevoid { value() { return undefined; }, get(path: string) { return NULL_REFERENCE; } }; type Primitive string | number | boolean | void; class PrimitiveReferenceT extends Primitive implements PathReferenceT { private innerValue: T; constructor(value: T) { this.innerValue value; } value(): T { return this.innerValue; } get(path: string): PathReferencevoid { return NULL_REFERENCE; } }PrimitiveReference利用原始值上任何路径查找都必然是undefined这一信息在get()中直接返回共享的常量NULL_REFERENCE既省去了逐层构建子引用的开销也让下游结果稳定可复用。示例二hashhelper 与HashReferenceEmber 的hashhelper 接收命名参数并将其转换为hash字典对象user-profile user{{currentUser}} options{{hash compactfalse metrue}} / {{#each currentUser.friends as |friend|}} user-profile user{{friend}} options{{hash compacttrue mefalse}} / {{/each}}在user-profile组件内部options可以像普通属性一样访问div classuser-profile h3{{user.name}}/h3 {{#unless options.me}} mutual-friends user{{user}} / {{/unless}} {{#unless options.compact}} ... {{/unless}} /divhashreference 的简化实现class HashReference implements PathReferenceDictionaryany { private args: DictionaryPathReferenceany; constructor(args: DictionaryPathReferenceany) { this.args args; } value(): Dictionaryany { let dict new Dictionaryany(); Object.keys(this.args).forEach((name) { dict[name] this.args[name].value(); }); return dict; } get(path: string): PathReferenceany { return this.args[path] || NULL_REFERENCE; } }通过实现PathReference接口HashReference在响应简单路径查找如上面示例中的{{options.me}}时可以避免构造Dictionary对象也就避免了对所有未使用引用求值——这是拉取 惰性哲学在真实模板特性中的又一次落地get(me)直接返回args[me]对应的子引用而无需先把整个 hash 对象物化出来。仓库中的真实实现从概念到代码上面是文档中的教学化模型在实际仓库中这些概念由glimmer/reference包实现核心文件为 packages/glimmer/reference/lib/reference.ts。接口定义的落地Reference的正式类型定义位于 packages/glimmer/interfaces/lib/references.d.tsexport type ConstantReference 0; export type ComputeReference 1; export type UnboundReference 2; export type InvokableReference 3; export type ReferenceType | ConstantReference | ComputeReference | UnboundReference | InvokableReference; export interface ReferenceT unknown { [REFERENCE]: ReferenceType; debugLabel?: string | false | undefined; compute: Nullable() T; children: null | Mapstring | Reference, Reference; }可见实际实现比教学接口丰富每个引用带有一个ReferenceType标记CONSTANT/COMPUTE/UNBOUND/INVOKABLE、可选的compute函数、以及一个子引用缓存childrenMap——这正是文档中PathReference.get思想的工程化形态子引用被缓存避免重复创建。工程化的引用构造器packages/glimmer/reference/lib/reference.ts 提供了一组工厂函数对应文档中的各类引用createPrimitiveRef(value)创建原始值引用打上UNBOUND标记并挂上CONSTANT_TAG常量标签值永远不变createConstRef(value, debugLabel)创建常量引用CONSTANTcreateUnboundRef(value, debugLabel)创建不可追踪值引用UNBOUND同样挂常量标签createComputeRef(compute, update?, debugLabel?)创建计算引用COMPUTE这是最通用的形式compute即文档中的value()逻辑createInvokableRef(inner)创建可调用引用INVOKABLE把读valueForRef与写updateRef都委托给内层引用。文档中的NULL_REFERENCE、UNDEFINED_REFERENCE、TRUE_REFERENCE、FALSE_REFERENCE也是真实存在的共享单例见 reference.ts并且isConstRef、isUpdatableRef、valueForRef、updateRef等工具函数构成了引用系统的公共 API全部经由 packages/glimmer/reference/index.ts 导出。valueForRef带缓存的拉取值教学模型中每次value()都重新计算真实实现则结合了 Validators 一章的 tag 系统做增量计算。valueForRef的核心逻辑reference.ts可概括为若 tag 为CONSTANT_TAG直接返回缓存的lastValue否则检查lastRevision与 tag若 tag 无效数据已变化则在track()中重新执行compute()并记录新 tag 与 revision若 tag 仍有效直接复用lastValue最后consumeTag(tag)把本次读取登记到当前验证上下文中。从源码结构看这就是文档拉取式 惰性模型与无通知追踪承诺之间的桥梁值的拉取本身会携带 tag 消费信息为上层判断哪些引用被读取过提供依据。childRefFor路径到子引用的转换文档中PathLookupReference的工程化版本是childRefForreference.ts若父引用是UNBOUND类型直接读取父值并取path属性创建createUnboundRef子引用非对象则返回UNDEFINED_REFERENCE对应文档的软失败语义否则创建一个createComputeRef子引用求值时valueForRef(parent)后再getProp(parent, path)更新时setProp(parent, path, val)子引用会被缓存进父引用的childrenMap同一路径的多次查找复用同一个子引用childRefFromParts则把[user,name,first]这样的路径片段逐段childRefFor下去等价于文档中path.split(.)后递归查找。运行时字节码中的引用操作在 packages/glimmer/runtime/lib/compiled/opcodes/expressions.ts 中可以看到引用如何在 VM 指令层面被消费VM_GET_VARIABLE_OP把vm.referenceForSymbol(symbol)压入操作数栈——读取一个变量符号对应的 ReferenceVM_GET_PROPERTY_OP弹出栈顶引用执行childRefFor(expr, key)后压回——这正是模板中{{user.name}}这类路径查找在运行时展开为父引用 子引用的地方VM_SET_VARIABLE_OP用scope().bindSymbol把引用绑定到作用域符号。由此可以确认文档所述模型与真实执行路径完全吻合模板的动态段被编译为引用路径查找被编译为childRefFor调用来构建引用链渲染时通过逐级valueForRef拉取最新值。迭代与#each引用系统的又一应用packages/glimmer/reference/lib/iterable.ts 展示了引用系统在{{#each}}中的运用createIteratorRef(listRef, key)基于列表引用创建迭代器引用其value()返回一个惰性的ArrayIterator或IteratorWrappercreateIteratorItemRef则为每个迭代项创建可更新的引用配合key/index/identity等 key 策略生成稳定身份用于 DOM 复用避免列表重排时丢失 DOM 状态。这从侧面印证了引用是 Glimmer 一切动态值的统一载体这一设计。与 Validators 的衔接下一块拼图本文反复提到tag 系统其详细设计在 Validators 一章展开由于 Reference 封装的计算可能任意昂贵应避免不必要地重算value()。Glimmer 的 validators 系统为每个引用关联一个实体标签EntityTag类似 HTTP 的 ETag 机制用value()/validate()判定计算结果的新鲜度更进一步RevisionTag基于全局递增 revision 计数器实现常量标签返回 0、易变volatile标签返回NaN作为毒药使整条链失效、当前标签返回全局计数器的当前值。glimmer/validator包中的CONSTANT_TAG、createTag、validateTag、consumeTag、track等原语正是 reference.ts 中valueForRef缓存逻辑的依赖。可以说Reference 解决了如何描述随时间变化的值Validator 解决了如何低成本地判断值是否变化二者配合构成了 Glimmer 无需订阅/通知的脏检查系统。总结Reference 是 Glimmer 运行时的核心原语一个只暴露value(): T的稳定对象表示随时间变化的纯计算结果它是拉取式的无订阅与通知。可组合、可泛化通过组合与组合子AdditionReference、map等任意复杂计算都能建模为引用链拉取式语义天然支持惰性求值如条件引用的短路。PathReference是模板路径查找的关键扩展get(path)把路径查找转换为子引用使父引用可以向下传播上下文信息原始值 →NULL_REFERENCE、hash → 直接取参数引用并避免无谓物化与求值。工程实现完整可查glimmer/reference的ReferenceImpl与工厂函数、valueForRef的缓存机制、childRefFor的子引用缓存以及 VM 指令VM_GET_PROPERTY_OP对childRefFor的调用都是文档模型的真实落地。与 Validators 配套使用引用负责拉取tag 负责判断是否过期共同支撑 Glimmer 高效的按需重渲染。继续阅读下一章Validators »如需回顾整体运行流程可返回 运行时总览 或从头阅读 Glimmer 内部指南简介。【免费下载链接】ember.jsEmber.js - A JavaScript framework for creating ambitious web applications项目地址: https://gitcode.com/gh_mirrors/em/ember.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考