Files
jscriptx/script_static.go
T
what 7e1893b246 refactor: 文件按主类型划分,一个 struct 一个功能
约定:文件名 = 主类型名;同一个类型要拆多个文件时用 类型_子项.go。

  engine.go          355 → 231 行。原来混了三件不相干的事
  engine_globals.go  ← bind / lazyGlobal / freeze / defineReadOnly(96 行)
                       它是「Go 值 → 只读 JS 全局」的转换层,跟脚本缓存毫无关系
  engine_console.go  ← console.go,跟上面是同一主题
  script.go          325 → 100 行,只留公开方法
  script_vm.go       ← VM 的取、还、装载。上个 commit 合一的三条路径现在住一起
  script_static.go   ← static.go
  errors.go          364 → 150 行
  errors_goja.go     ← goja 错误的翻译层
  errors_hints.go    ← missingGlobalHint,一份 JS 运行时知识库,跟错误分类是
                       两回事;拆出来之后 missing_global_test.go 才有对应源文件
  bundle_finalize.go ← esmwrap.go
  bundle.go          收下 validIdent / hashVersion(原来住在 engine.go)

Engine.New 原来排在所有私有函数之后,挪到导出方法那一段。

classify 83 → 54 行:四个 errors.As 分支各手搓一个 8 字段的 &Error{},脚本上下文
那三行重复了 4 遍,抽出 gojaError 构造器。

顺带修四处注释漂移:

  Session/会话   代码里叫 Instance,注释里大面积残留。engine_globals.go 那条
                 「注入会话全局对象失败」还是用户可见文案,而公开 API 里根本
                 没有「会话」这个概念
  Extension      文档示例写 Module() string,接口是 Module() (path, source string)。
                 这是唯一一段教人写扩展的文档,照抄编译不过
  doc.go 的 freeze  说白名单「逐层拷贝成只读对象,不会跨 VM 共享可变的 Go map」,
                 但那只对 map[string]any 成立。结构体指针和 slice 是**共享同一个
                 对象**的——扩展走的正是这条路,不该被当成隔离保证
2026-09-10 15:28:59 +08:00

86 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jscriptx
import "context"
// CallStatic 调用脚本导出的 class 上的**静态方法**。
//
// export default class PkgImportController {
// static Startup() { store.Set("pkg.registry", "https://…") }
// Execute(g) { … }
// }
//
// _, err := script.CallStatic(ctx, "Startup")
//
// 它跟 Call 有两点不同,都是"静态"这个语义要求的:
//
// - **不构造实例**constructor 不会跑。构造函数做的是"这一次调用的准备",
// 跟脚本级的初始化无关,跑它只会白费一遍还可能有副作用。
// - **VM 用完就丢**,不回池。静态方法一般只在脚本生命周期里跑一两次,
// 为它留一个 VM 不划算;而且它做的事通常是往 Go 侧扩展里写东西,
// 那些副作用留在 Go 那边,VM 本身没有保留的价值。
//
// 脚本没写这个静态方法时返回的错误可以被 errors.Is(err, ErrFuncNotFound) 匹配上
// ——生命周期钩子多半是可选的,用它来区分"没写"和"写了但炸了"
//
// if _, err := script.CallStatic(ctx, "Startup"); err != nil &&
// !errors.Is(err, ErrFuncNotFound) {
// return err // 真的出错了
// }
//
// 静态方法里能用 ctx 带进来的作用域扩展(WithScope),这正是它的用武之地:
// 把配置写进 store、往 Go 侧注册东西。但**它建的 JS 对象活不下来**——
// VM 一丢就没了,别指望后续调用能看到。
func (s *Script) CallStatic(ctx context.Context, fn string, args ...any) (any, error) {
if fn == DefaultFunc {
return nil, newError(KindNotFound, s.name, fn, ErrFuncNotFound,
"CallStatic 要给一个静态方法名")
}
return callAny(ctx, staticTarget{s}, fn, args)
}
// HasStatic 判断脚本导出的 class 上有没有这个静态方法。
//
// 它要建一个 VM 才能回答,所以别在热路径上反复调;只想"有就调"的话,
// 直接 CallStatic 然后判 ErrFuncNotFound 更省。
func (s *Script) HasStatic(fn string) bool {
if fn == DefaultFunc {
return false
}
vm, err := s.borrowStatic(context.Background())
if err != nil {
return false
}
// 建了 VM 就得记一笔丢弃,走跟 CallStatic 同一套记账。漏掉的话每调一次
// Stats().Created - Dropped 就永久 +1
defer staticTarget{s}.finish(vm, true)
// ctor != nil 排除 lookup 的全局兜底分支:静态方法必须挂在 class 上,
// 脚本导出的不是 class 时 lookup 可能在全局里找到同名函数
_, _, _, ok := vm.lookup(fn)
return ok && vm.ctor != nil
}
// staticTarget 让静态调用复用 invoke 那一整套(超时中断、panic 恢复、错误分类),
// 只是换一种取 VM 的方式:不构造实例,用完丢弃。
type staticTarget struct{ s *Script }
func (t staticTarget) owner() *Script { return t.s }
func (t staticTarget) acquire(ctx context.Context) (*vmHandle, error) {
return t.s.borrowStatic(ctx)
}
// finish 直接丢弃,不回池——见 CallStatic 的说明。
func (t staticTarget) finish(*vmHandle, bool) { t.s.dropped.Add(1) }
// borrowStatic 造一个只装了 class、没有实例的 VM。
func (s *Script) borrowStatic(ctx context.Context) (*vmHandle, error) {
if s.closed.Load() {
return nil, newError(KindClosed, s.name, "", ErrClosed, "脚本已关闭")
}
sc, _ := scopeOf(ctx)
// noInstance:静态方法挂在 class 上,不用白跑一遍 constructor
return s.newVM(ctx, sc, nil, true)
}