35 KiB
框架如何接管代码执行
框架通过保存定义、关联入口、管理活动状态和调用用户函数来组织执行。语言、执行引擎、宿主和框架的职责分别说明,才能判断一段代码何时运行、在哪运行,以及哪个实体一直存续。基础结构见12 门语言的执行模型与生命周期,词法用途见关键词手册。
资料核对日期:2026-10-13。本文以普通 hosted C 进程、CPython/FastAPI/ASGI、Java/Spring Boot/Spring MVC、Unity 6.0,以及 TypeScript/React/Node/Chromium 的指定路径为案例。持续更新的官方文档与实现源码用于说明相应机制,不代表所有历史版本。代码展示预期行为,未作为执行日志;框架的开发模式、配置和替代后端会改变部分时序。
先确定每一层回答什么
| 层 | 职责 | 典型证据 |
|---|---|---|
| 语言 | 调用、作用域、暂停、异常、类型等的语义 | Python、Java、C#、ECMAScript 等规范 |
| 编译或构建工具 | 检查和转换源码,准备可执行逻辑 | TypeScript 类型擦除、JSX 转换、C# 编译 |
| 执行引擎或原生执行环境 | 推进当前代码并维护必要执行状态 | CPython、JVM、V8、Mono、原生机器码 |
| 宿主 | 应用入口、I/O、系统事件、线程或事件循环、呈现能力 | ASGI 服务器、Servlet 容器、Node、浏览器、Unity |
| 库或框架 | 入口映射、应用状态、组件或请求的组织 | FastAPI 路由、Spring bean、React 组件树 |
这些是职责边界,不要求每层都是一个独立进程,也不保证每个产品只处在一层。Unity 同时提供引擎宿主与组件管理;Node 包含 V8、原生绑定及 libuv 等协作部分;浏览器还有渲染、网络及多进程组织,不能缩减成 V8 本身。
注册建立“条件与逻辑”的关系。外部事件或明确调用进入入口;调度机制安排执行机会;引擎执行本次函数。框架也会执行自己的代码,它调用用户函数时通常仍使用语言的普通调用机制。
路径一:C 普通进程
从启动到返回
这是一个完整的 hosted C 示例,不依赖应用框架。预期输出 42 并正常返回成功状态。
#include <stdio.h>
static int price(int base) {
return base + 2;
}
int main(void) {
int result = price(40);
printf("%d\n", result);
return 0;
}
启动程序
→ 操作系统及实现的启动代码准备执行环境
→ 进入 main
→ 用户调用 price,当前线程进入该函数
→ price 返回 42,继续 main
→ printf 产生输出
→ main 正常返回,执行正常退出流程
price 的函数定义不因“程序已经启动”而自己反复执行。这里没有应用任务队列:下一步由当前控制流确定;CPU 最终执行编译后的指令,OS 仍可调度执行线程。C23 草案的 hosted 启动与终止
谁持有状态
| 实体 | 保存的信息 | 结束方式 |
|---|---|---|
| 程序及静态存储 | 可执行代码与长期数据 | 正常退出或进程终止 |
main 调用 |
result、当前进度及调用关系 |
返回 |
price 调用 |
base 及临时计算 |
返回 42;典型原生调用状态结束 |
| 输出设施 | 缓冲和 OS 资源 | 库及退出协议处理 |
实际 ABI 可以用栈、寄存器和内联等实现调用。语言规定调用结果,具体执行布局由实现决定。添加 GUI 库或 HTTP 服务器后,入口还会增加库管理的事件关系;C 语言本身没有因此改变。
路径二:FastAPI
注册路由与处理请求是两个动作
以下内容是可交给 ASGI 服务器加载的 Python 模块。sleep 用于展示可暂停等待,不代表真实数据库实现。预期请求 GET /price?base=40 得到 JSON 数据 {"price":42}。
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/price")
async def price(base: int = 40):
await asyncio.sleep(0.01)
return {"price": base + 2}
加载模块时执行导入、创建 app、创建函数及应用装饰器;装饰器把路径、方法和函数等关联起来。它不在注册时替未来每个请求执行 price 函数体。请求进入后,框架才使用这份关联选择处理函数。
服务器以 Uvicorn 为例;事件循环可使用 asyncio 或其兼容实现,而非只能使用 CPython 中某个唯一固定循环。Uvicorn 事件循环选择
一次请求经过哪些层
sequenceDiagram
participant Client as 客户端
participant Server as ASGI 服务器与事件循环
participant App as FastAPI 与 Starlette
participant Handler as 用户 price 协程
Client->>Server: GET /price?base=40
Server->>App: 调用应用:scope、receive、send
App->>App: 路由、参数与依赖处理
App->>Handler: 调用并推进协程
Handler->>Server: 可暂停等待
Note over Handler: 局部状态和继续关系被保留
Server->>Handler: 条件满足后安排继续
Handler-->>App: 返回字典
App->>Server: 生成并发送响应事件
Server-->>Client: HTTP JSON 响应
图示压缩了中间件和序列化细节。ASGI 的 HTTP scope 对应一次请求,WebSocket scope 则可贯穿整个连接;HTTP keep-alive 不把多个请求合并为同一次处理函数调用。服务器提供协议事件,应用通过可等待的 receive/send 接收与发送。ASGI 规范
| 结构 | 保存的信息 | 管理者与存续 |
|---|---|---|
模块及 app、路由定义 |
应用配置和入口映射 | 模块/框架,跨多个请求 |
| 请求 scope、Request、依赖求值状态 | 方法、路径、头、输入及本次依赖 | 服务器/Starlette/FastAPI,围绕请求 |
| coroutine 继续状态 | base 等局部状态、等待后的进度 |
Python 执行机制,围绕本次处理 |
| Task 及事件循环的等待管理 | 活动状态、等待结果和恢复安排 | ASGI 服务器选定的异步环境 |
| 响应对象与输出事件 | 状态码、头及 body | 框架/服务器,直至相应发送流程结束 |
同步函数和异步函数的执行位置
FastAPI 通过框架调用的普通 def 路由函数和同步依赖,通常交给外部线程池;async def 路由在异步调用链中推进。这个区别只适用于它负责调用的设施:用户自己在 async 路由中直接调用普通辅助函数,仍是普通同步调用,不自动被挪到线程池。异步函数中的 CPU 循环或阻塞 I/O 仍能阻碍事件循环。FastAPI 的技术说明
应用、请求和依赖分别清理
应用的 lifespan 上下文管理共享资源:进入部分用于应用接收请求前的准备,退出部分用于关闭。带 yield 的依赖围绕更小的工作范围获取和释放资源。当前文档区分 scope="function" 的函数退出清理与 scope="request" 的响应发送后清理;不要把两者写成一个固定时间点。应用 lifespan、带 yield 依赖的 scope
处理函数返回不意味着应用退出;请求结束不意味着共享连接池必须销毁。失败进入异常处理路径,取消及断连的传播要结合服务器、中间件和被等待操作判断,不能承诺客户端断开后所有业务效果立即停止或回滚。
路径三:Spring Boot
从入口启动容器
基线是使用 Spring MVC 的应用及嵌入式 Servlet 容器,Tomcat 可作为例子。Spring Boot 组织启动与配置;Spring 的 ApplicationContext 管理 bean;Servlet 容器接入 HTTP;JVM 执行 Java 方法。这些不是同一个生命周期。
下面是 example.pricing 包中的 PriceApplication.java 示例,前提是项目具有相应 Spring Boot Web 依赖。预期请求 GET /price?base=40 返回包含 price: 42 的 JSON 对象。
package example.pricing;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class PriceApplication {
public static void main(String[] args) {
SpringApplication.run(PriceApplication.class, args);
}
@RestController
static class PriceController {
@GetMapping("/price")
public Map<String, Integer> price(
@RequestParam(name = "base", defaultValue = "40") int base) {
return Map.of("price", base + 2);
}
}
}
启动时由容器读取配置、发现并创建所需 bean、注入依赖及调用相应初始化设施。映射注解是供框架使用的元数据;Java 的 @GetMapping 拼写不是新增 Java 关键词,声明它也不等于马上处理请求。Spring Boot 的启动、bean 与依赖注入
请求怎样进入 controller
HTTP 请求
→ Servlet 容器接入并安排处理
→ Filter 等链路
→ DispatcherServlet
→ HandlerMapping 查找匹配的处理器
→ HandlerAdapter 配合参数解析等设施调用 controller 方法
→ JVM 推进本次 Java 调用
→ 返回值处理及消息转换形成 JSON 响应
→ 容器写回客户端
这里选择 @RestController 的数据响应路径;返回视图名的 controller 还会涉及视图解析。URL 没有一个“执行 Java”的特殊开关,服务端配置的分派逻辑决定产生响应的方法。DispatcherServlet、请求处理序列
| 结构 | 保存的信息 | 管理者与存续 |
|---|---|---|
| JVM 的类、方法元数据 | 编译后的 Java 逻辑 | JVM/类加载器 |
| bean definition 与 ApplicationContext | 创建配方、依赖关系及受管理实例 | Spring,跨请求或按配置的 scope |
| 请求映射与 handler 元数据 | 路径、方法、参数及调用入口 | Spring MVC,跨请求 |
| Servlet 请求/响应及分派状态 | 本次 HTTP 数据和处理关联 | 容器及框架,围绕一次请求 |
| JVM 方法 frame | 参数、局部变量及方法进度 | 本次 Java 调用 |
默认 singleton bean 在容器内被复用,不是每个请求都创建一个 controller。方法 frame 是本次调用的状态;把请求数据存入复用实例字段会涉及并发访问。这是实例存续与请求存续不同带来的后果。prototype 等 scope 的退出管理也有不同保证,不能认为所有 bean 都由容器完整管理至销毁。Spring bean scope
初始化、请求返回与关闭
Spring 可以调用 @PostConstruct、初始化接口或配置的 init 方法;正常关闭时可调用 @PreDestroy 等销毁设施。Lifecycle/SmartLifecycle 还表达受管理活动的启停。回调由容器在相应条件下调用,不由 Java GC 替代;具体 bean scope 的管理保证需要分别核对。Spring bean 生命周期
请求方法返回后应用继续存续。Boot 的正常退出支持关闭 ApplicationContext 的安排;强制终止不能被当作所有清理钩子都会执行的正常关闭。Spring Boot 应用退出
WebFlux 是另一种请求模型
Spring Boot 并不只代表 Servlet 阻塞模型。WebFlux 使用非阻塞契约、响应式组合及服务器适配;通常用较少的事件循环工作线程推进活动,和 MVC 常见的阻塞处理假设不同。Mono/Flux 保存或组合数据流协议,不等同于每个请求专属的一条 JVM 调用栈。这里的 Reactor Mono 是响应式库类型,与 Unity 的 Mono 执行后端无关。MVC 也能使用异步设施,不能把二者概括成“Java 一定阻塞”和“Java 一定事件循环”。Spring WebFlux 的并发模型
路径四:Unity
执行后端与阶段安排是两个问题
Unity 的 C# 脚本通常先编译为 IL 等内容。Mono 后端提供托管执行与 JIT;IL2CPP 将 IL 转为 C++ 再编译为原生代码。不能直接把 Unity 写成桌面 CoreCLR 应用。无论采用哪条后端路径,引擎依然承担场景与组件管理,以及 PlayerLoop 的阶段调用安排。Unity 脚本后端
开发准备:C# 源码 → IL → Mono 执行路径/IL2CPP 原生编译路径
应用运行:Unity 启动与场景准备
→ 建立 GameObject 与组件实例及关系
→ 根据对象活动、组件启用与阶段规则调用事件函数
→ PlayerLoop 推进输入、物理、脚本更新及其他系统
→ 场景卸载、禁用或销毁触发相应处理
PlayerLoop 是引擎的一组系统阶段,还能通过相关 API 定制;并不是每一帧把项目里所有 C# 函数都运行一次。Unity 事件执行顺序
哪些回调在什么条件下进入
| 入口 | 管理条件 | 不能推出的结论 |
|---|---|---|
Awake |
组件实例加载、对象激活等符合条件的初始化,一实例调用一次 | 不是所有不活动对象都在场景加载时立即执行,也不是 C# 普通构造器 |
OnEnable |
对象活动且组件启用时 | 不是一生只调用一次,可因再次启用进入 |
Start |
启用后首次更新前的一次初始化 | 不保证与 Awake 同帧;禁用时可能延后 |
FixedUpdate |
固定时间步的物理更新安排 | 每个显示帧可能零次、一次或多次 |
Update |
符合条件的脚本帧更新 | 不是全部数据对象的通用钩子 |
LateUpdate |
在 Update 后的相应阶段 | 不代替任意线程的同步协议 |
OnDisable |
组件或对象退出启用活动等相应情形 | 禁用和销毁不是同一状态 |
OnDestroy |
满足条件的组件或对象销毁 | 有适用边界;强制退出不能保证执行 |
条件详见 Awake、OnEnable、Start、FixedUpdate、Update、LateUpdate、OnDisable、OnDestroy。
以下是应保存为 PriceLabel.cs 并挂在活动 GameObject 上的组件例子。这里用日志观察执行,不代表 UI 的正式绘制实现。
using UnityEngine;
public class PriceLabel : MonoBehaviour
{
private int price;
private void Awake() { price = 42; }
private void OnEnable() { Debug.Log("enabled"); }
private void Start() { Debug.Log(price); }
private void Update() { /* 每次符合条件的更新处理 */ }
private void OnDisable() { Debug.Log("disabled"); }
}
在常规初始化条件下,实例准备→Awake→OnEnable→Start→后续 Update。重新禁用再启用,可以再发生 OnEnable;这不意味着 price 字段被重新创建或 Start 再次执行。同类不同对象的调用顺序不能仅凭源码文件排列推断。
哪些状态跨帧存在
| 结构 | 保存的信息 | 管理者与存续 |
|---|---|---|
| 场景、GameObject 与组件关系 | 对象组织、活动及启用状态 | Unity 引擎,跨帧 |
| 脚本实例及字段 | price 等长期组件状态 |
托管对象及引擎对象关联,跨回调 |
| 每次 Update 等的方法调用状态 | 本次局部变量和当前进度 | 执行后端,一次回调 |
| IEnumerator 状态机与协程等待记录 | 跨 yield 的局部数据、继续位置及等待条件 | 编译器表示及 Unity 协程调度 |
| 引擎资源与托管引用 | 场景对象、资源及引用关系 | 引擎销毁和托管回收分别处理 |
普通 Update 返回后,下帧是另一次函数调用;Unity 协程 yield return 后则由引擎按等待条件继续迭代状态。StartCoroutine 不自动创建后台线程;同步耗时计算仍可占用主线程。停止条件也取决于对象、组件及协程 API,不能把“组件 disabled”当成所有任务统一取消协议。Unity 协程
路径五:React
构建不是应用运行的中间调度层
开发及构建:
TS/TSX 源码
→ 类型检查、类型处理、JSX 转换及模块处理
→ 供目标宿主执行的 JavaScript
运行:
浏览器:启动 JS → React 组织组件计算 → DOM 提交 → 浏览器呈现
服务器:Node 执行 JS → React 服务端渲染 → 输出 HTML 等响应
转换可以分散在 TypeScript、其他转换器和打包工具中;JSX 也可以先被保留。构建工具运行在 Node,不证明组件随后一定在 Node 执行。React 是正在被执行的库代码,会组织用户组件及更新的调用;TS 类型通常已擦除。TypeScript 的类型擦除、JSX 转换模式
浏览器首次显示与一次点击
下面是客户端入口 TSX 的例子,前提是页面中有 id="root" 的容器,且构建环境提供 React、React DOM 和相应类型声明。它是普通客户端渲染,不含 SSR。
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
function Price() {
const [price, setPrice] = useState(42);
useEffect(() => {
const previousTitle = document.title;
document.title = `Price: ${price}`;
return () => { document.title = previousTitle; };
}, [price]);
return <button onClick={() => setPrice(p => p + 1)}>{price}</button>;
}
createRoot(document.getElementById("root")!).render(<Price />);
预期首次按钮显示 42,点击后显示 43;提交后的 Effect 同步相应标题。示例的非空断言不构成运行时容器创建,HTML 必须实际提供该节点。调用 root.render 提交渲染请求,也不能据此认为调用返回时所有 DOM 和 Effect 工作已经完成。createRoot 与 render
| 步骤 | 谁进入或调用 | 保存或使用的状态 |
|---|---|---|
| 加载入口 JS | 浏览器的脚本加载与求值机制 | JS 模块、函数及 React 库 |
| 首次 render 请求 | 入口调用 React root API | root、更新及组件关系 |
| 组件计算 | React 调用 Price |
本次调用的局部值,React 持有的 Hook 状态 |
| commit | React 调用 DOM 等宿主接口 | 已提交的树及 DOM 关系 |
| Effect setup | React 按 Effect 规则安排 | 依赖和返回的 cleanup |
| 用户点击 | 浏览器事件分派进入 React 事件处理,再调用用户处理逻辑 | 事件、组件关联及更新请求 |
| 后续更新 | React 处理状态更新,再组织 render/commit | 新状态,旧依赖及新依赖 |
| 卸载 | root 或组件树操作使组件退出 | Effect cleanup、宿主节点及关联清理 |
render 是计算下一份界面描述,commit 才将相应变化应用到宿主。组件函数可以被多次调用;应保持渲染计算纯净,不能把“函数被调用一次”当作“一次业务操作已经提交”的保证。React render 与 commit、组件与 Hook 的纯度要求
状态为什么没有随组件函数返回而消失
React 持有组件关系及 Hook 状态,组件函数每次调用读取当前状态。普通局部变量随调用建立,useState 管理的状态则与 React 管理的组件身份关联,不能当作每次新建的普通局部值。React 状态的保留与重置
| 结构 | 职责 | 所属层 |
|---|---|---|
| JS 函数、闭包和本次调用状态 | 执行组件计算及事件函数 | JavaScript 引擎 |
| React element 等界面描述 | 表达本次计算所得结构 | React 库数据 |
| Fiber 与 root | 组件工作、父子关联、已保存状态、更新及待处理工作 | React 当前协调实现 |
| Hook 状态与 Effect 记录 | 状态、依赖及 cleanup 关联 | React 管理结构 |
| DOM 节点与事件设施 | 浏览器中的宿主呈现及交互 | 浏览器 |
Fiber 是实现结构,源码包含子节点、相邻节点、更新队列和保存状态等信息。它不等于 V8 调用栈,也不是所有 React API 使用者必须直接创建的对象;字段和布局不是稳定应用 API。React 实现中的 Fiber 类型
React Hook 与 Unity 阶段回调的差别
useState、useEffect 等普通 Hook 必须服从顶层调用与顺序约束,它们不是可以任意在条件中增删的动态回调列表。useEffect 记录的 setup/cleanup 与依赖变化关联;useState 则管理状态,并不代表事件监听。特殊的 use API 有不同的调用规则,不能用“所有 use 开头函数都绝对相同”概括。Hook 规则、use API
Effect 在组件提交后按相关规则同步外部系统;依赖变化时先清理旧关联,再建立新关联,卸载时清理。开发 Strict Mode 会增加用于检查的 setup/cleanup 周期。Effect 与浏览器实际绘制的先后不能简单固定为“每次 paint 之后”;useEffect 与 useLayoutEffect 的契约不同。useEffect 的时序与边界
Unity 主要用引擎命名阶段选择符合条件的组件回调;React 主要用更新、组件计算、提交和依赖关联组织 UI。React 不要求组件每个显示帧都重新计算,也没有为每个普通函数自动提供 Awake/Update。
可选的服务端渲染与 hydration
sequenceDiagram
participant Browser as 浏览器进程
participant Server as Node 服务器进程
participant SSR as 服务器 React 渲染
participant ClientReact as 浏览器 React
Browser->>Server: 页面请求
Server->>SSR: 调用服务端渲染入口
SSR-->>Server: HTML 流等结果
Server-->>Browser: HTTP 响应与后续资源
Browser->>Browser: 解析 HTML 并加载客户端 JS
Browser->>ClientReact: hydrateRoot 等入口
ClientReact->>Browser: 关联交互逻辑与已有 DOM
renderToPipeableStream 是 Node 流环境的一条 React 服务端输出路径;浏览器的 hydrateRoot 接管已有的服务端 HTML 并建立客户端交互。初始渲染结果要符合 hydration 的一致性要求,不能把服务器函数对象、闭包或堆地址当成直接转移到浏览器的状态。服务端流渲染、hydrateRoot
服务端和客户端分别拥有自己的 JS 执行环境与对象。Effects 不在服务端渲染时运行;需要客户端的交互逻辑仍由浏览器加载和执行。SSR、React Server Components、服务器函数与框架路由是不同机制;本节追踪传统 SSR/hydration,不把其他机制默认叠加进去。
Node 与 Chromium
请求得到什么与客户端怎么使用分别决定
服务端先按路由或静态资源配置生成响应,客户端再按请求用途、响应信息和自己的规则使用内容。URL 的后缀不能单独决定代码是在服务器执行还是在浏览器执行。
| 动作 | 服务端可能怎样生成响应 | 浏览器接收后的典型路径 |
|---|---|---|
| 导航到页面 URL | 静态 HTML 或程序生成 HTML | 导航及文档解析、样式、布局和呈现 |
页面引用 <script src="/app.js"> |
读取文件或程序生成 JS | 脚本机制在满足加载、类型及策略条件后安排执行 |
fetch("/app.js") |
也可能是同一份 JS 字节 | 得到 Response,读取为文本不自动执行该文本 |
fetch("/price") 并解析 JSON |
服务端处理函数计算再序列化 | 创建本地 JS 数据;用户代码决定怎样更新界面 |
脚本加载和执行规则由 HTML 宿主机制规定;Fetch 返回响应的 API 本身不包含“自动把响应当脚本执行”。直接打开 JS 或 JSON 资源时,显示或下载还受导航、媒体类型和响应头等规则影响,不能假定所有浏览器都采用相同展示界面。HTML script 规则、Fetch 方法
服务器即使返回 JSON,浏览器也可以显示文本;具有特定应用含义的按钮、图表等需要客户端程序、HTML 或相应查看器的呈现规则。JSON 的数据结构本身不会自动定义完整应用 UI。
浏览器与 Node 的任务管理
| 维度 | Chromium 中的页面 | Node 应用 |
|---|---|---|
| 入口 | 导航、脚本/模块、事件、控制台求值等 | 入口模块、REPL、服务器及其他 API 回调 |
| 系统事件 | 用户交互、网络、定时器、页面生命周期等 | 网络、文件、定时器、进程等 |
| 调度组织 | HTML 任务来源、任务队列、微任务检查点及渲染机会 | libuv 阶段、Node 回调边界及微任务处理 |
| 呈现 | DOM、样式、布局、绘制等浏览器设施 | 默认没有页面 DOM 和浏览器绘制阶段 |
| 长期状态 | 文档、DOM、监听关联、JS 环境、框架状态 | 模块、服务器、连接、监听关联、JS 环境 |
| 实际执行 JS | V8 | V8 |
浏览器规范中的正式概念是 task;“宏任务”是常见教学叫法。不能把浏览器描述成唯一全局 FIFO 队列。微任务检查点会处理新加入的微任务,渲染还要符合渲染机会条件。HTML 事件循环
Node 的 timers、poll、check 等阶段来自宿主机制;setImmediate 和 process.nextTick 不是 ECMAScript 关键词或 V8 自带的全部循环。nextTick 的先后还要考虑调用上下文及模块求值方式,不能承诺它在所有情境绝对先于一切 Promise 工作。Node 阶段与 nextTick
V8 提供上下文、代码执行和微任务相关能力,宿主通过嵌入接口配合调度。因此“V8 完全没有任何队列”也不准确;完整 I/O 与应用事件循环仍不能统称为 V8。V8 嵌入说明、V8 MicrotaskQueue API
同步调用、事件和 REPL 三种入口
Node HTTP 服务器把请求函数登记到服务器的 request 事件关系中;收到请求后,宿主和服务器设施进入回调,V8 执行用户 JS。运行到普通 helper() 调用时则立即进入该函数,不等待另一个 HTTP 事件。监听器是长期关联,每次调用状态是短期上下文。Node HTTP request 事件
Node REPL 管理输入流、求值上下文、结果输出及会话关闭等;浏览器控制台在所选页面或 frame 的环境中求值,能接触对应 DOM 等宿主对象。它们可以使用同一引擎的求值能力,但会话、绑定规则、可用 API 和后台活动不同。REPL 等输入时,进程不需要一直忙跑一段用户 JS 循环。Node REPL、Chrome 控制台执行 JavaScript
五条路径放在同一张表里
| 路径 | 触发与入口选择 | 执行的推进者 | 跨执行保留的主要状态 | 普通工作结束 |
|---|---|---|---|---|
| C 普通进程 | 启动入口、用户调用 | 原生代码及 OS 线程调度 | 程序数据,调用期间的状态 | 返回或退出 |
| FastAPI | 服务器接入、框架路由及依赖 | CPython、选定异步环境或线程池 | app、路由、请求、Task/协程等待状态 | 响应及相应范围清理 |
| Spring MVC | Servlet 容器、DispatcherServlet | JVM 与容器执行安排 | bean、映射、请求及方法状态 | 返回响应;容器继续存续 |
| Unity | 引擎状态与 PlayerLoop 阶段 | Mono/IL2CPP 路径及引擎 | 场景、组件字段、协程等待记录 | 回调返回、禁用或销毁分别发生 |
| React 浏览器 | root 更新、状态变化和事件 | React 组织调用,V8 执行,浏览器呈现 | 组件关系、Hook、更新、Effect、DOM | 计算完成、提交和卸载分别发生 |
钩子表达触发关系,调度器安排工作,解释器或其他执行机制推进代码;数据结构保存跨这些动作的关系。状态与迁移规则、归属与释放规则则决定哪些动作合法,以及活动何时结束。它们不能只用“有一个循环”替代。
对 Endorphin 执行模型的设计分析
本节提出研究维度及验证场景,不确认新的语法、原生任务类型或调度 API。现有语言决定仍以 Preflight 与 inflight、函数与调用、Object 与类型系统、资源访问与呈现、REPL 提案和规范状态为准。
先描述可观察的执行契约
| 维度 | 需要形成的明确契约 | 可用的验证场景 |
|---|---|---|
| 定义与活动 | 定义准备、对象创建、调用开始分别由什么动作发生 | 查看函数或导入是否触发业务调用 |
| 执行入口 | 显式调用、REPL 输入、界面事件和平台触发怎样进入 | 提交参数后何时真正执行定价函数 |
| 一次执行的单位 | 本次调用及其子调用、等待和结果如何关联 | 同一函数同时接受两个不同输入 |
| 上下文 | 代码与依赖、输入、局部状态、进度、结果接收方保存在哪里 | 等待后继续使用原来的输入 |
| 顺序与更新边界 | 哪些工作必须有序,何时发布可观察结果 | 同一事件引起两个更新,界面看到什么 |
| 等待与继续 | 保留本次状态、等待条件、恢复入口与失败传播 | 数据未就绪后继续执行,而非重复业务写入 |
| 并发与隔离 | 同一对象上的活动如何协调,哪些状态可共享 | 两次执行同时更新同一个运行对象 |
| 取消与归属 | 谁能结束活动,子活动和资源如何跟随 | REPL 关闭或 UI 消失后仍在等待的计算 |
| 位置与权限 | 平台怎样决定执行位置和有效能力 | 同一函数被不同身份使用,跨端对象如何关联 |
| 清理与保存 | 活动结束、资源解除、结果留存和对象回收分别何时发生 | 查看历史结果不重放计算,订阅结束能释放关联 |
执行记录可以引用平台对象,但数据对象、函数定义、调用状态、结果句柄、触发关联的职责仍需区分。拥有平台 ID 并不自动意味着需要执行;存储局部变量也不自动意味着具备跨进程恢复能力。持久化正在执行的状态还涉及代码版本、外部操作及恢复边界,不能从“状态可保存”直接推出可靠重启。
用一条轨迹检查规则是否完整
下面是研究用的行为轨迹,不是 Endorphin 源码或新增 API。
定义已准备
→ 用户明确提交一组输入
→ 建立本次执行及有效上下文
→ 获得执行机会并进入函数
→ 遇到未就绪操作,保留进度与等待关联
→ 条件满足后再次获得执行机会
→ 从继续位置推进,交付结果或失败
→ 解除本次活动资源;按规则保留结果或关联
这条轨迹至少能区分“输入还没准备好”“已经执行但正在等待”“已经完成而结果仍被查看”。如果界面重新呈现结果不应重新运行写入,就需要明确结果查看与调用触发的边界;如果不允许自动重试,就不能以重算替代恢复。
再选择队列与钩子的组织方式
以下是设计比较,不是已采用方案。
| 候选组织方式 | 适合表达的要求 | 必须另外处理的边界 |
|---|---|---|
| 直接同步调用 | 明确调用关系及立即返回 | 长时间运行、等待及并发活动 |
| 固定阶段回调 | 稳定周期或固定阶段的工作 | 不活动对象、阶段顺序及启停规则 |
| 事件订阅 | 按事件选择处理逻辑 | 订阅所有者、取消、重复事件及积压 |
| 可暂停任务与继续关系 | 等待后保留本次计算的进度 | 调度、公平性、失败、取消及资源归属 |
| 组件更新与提交边界 | 把计算与可观察界面更新关联 | 可重算性、副作用和结果发布时机 |
| 分层队列或检查点 | 区分不同工作的顺序及更新边界 | 饥饿、重入、无限连锁更新及跨队列顺序 |
宏任务/微任务只是一种组织顺序的具体办法。Endorphin 是否需要类似机制,应由事件处理、继续计算和结果呈现之间的顺序要求决定。固定阶段与依赖驱动的更新也可以服务不同实体,不需要先把所有对象放入一个通用循环。
远程数据同步可用于检查跨端身份、变化到达和更新顺序;它只是验证场景之一。执行模型的主问题仍是:入口怎样建立活动,执行如何推进,等待怎样保存并继续,以及活动由谁结束。