尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hyperapp 数据流全景图:读懂 init、Action、Effects 与 Subscription 的完整闭环

Hyperapp 数据流全景图:读懂 init、Action、Effects 与 Subscription 的完整闭环 前端【免费下载链接】hyperapp1kB-ish JavaScript framework for building hypertext applications项目地址https://gitcode.com/gh_mirrors/hy/hyperapp点击查看免费下载导读本文以 docs/architecture/flowchart.md 中的官方 Mermaid 数据流图为骨架逐节点拆解 Hyperapp一个 1kB 级别的超文本应用框架从启动到渲染、从事件到状态更新的完整运行循环。读完本文你将掌握 Hyperapp 的四种init初始化形态、Action 递归分发dispatch机制、next state之后的三条并行分支视图重渲染、订阅重算、副作用执行以及 Effect 通过 dispatch 重新注入 Action 形成闭环的底层原理并能在 index.js 源码中找到每一环对应的实现。一、总览一张图读懂 Hyperapp 的运行时架构docs/architecture/flowchart.md本质上是一张完整的状态机 / 数据流图Mermaidgraph TD它把 Hyperapp 应用从出生init到不断自我更新的全过程画成了一根事件 → Action → 状态 → 三路分发 → 回到 Action的闭环。全图如下需要说明的是图中的j0、j1、j2、j3是作者刻意放置的 1px 大小占位节点见图中style jN height:1px; width:1px;它们不参与任何逻辑纯粹用于控制 Mermaid 的连线布线方向帮助把多路汇入nextState和nextState多路扇出画得清晰。理解数据流时可以完全忽略它们。从整体看这张图可以归纳为三个组成部分三个事件入口初始化init、视图事件view event如 click、订阅事件subscription如 window resize一个核心枢纽Action状态变更的唯一合法通道它可能递归地派生出其他 Action也可能产生NextState一个状态扇出点next state它同时驱动DOM 重渲染、订阅重算、副作用Effects执行三条分支其中副作用又通过dispatch回灌新的 Action从而让整个系统周而复始地运转。二、起点init 的四种形态与两种去向流程图顶部的init节点是整个应用唯一的一次性启动入口它的出边被明确标注为两条init:{state} → 直接产生 next state init:[state, effect(s)] → 直接产生 next state并携带副作用 init:Action → 进入 Action 枢纽 init:[Action, payload?] → 进入 Action 枢纽这与 docs/api/app.md 中init:属性的四种合法形式一一对应init 形式含义示例init: state直接设定初始状态init: { counter: 0 }init: [state, ...effects]设定初始状态并立即运行副作用init: [{ loading: true }, log(Loading...)]init: Action让一个 Action 生成初始状态此时传入 Action 的 state 为undefinedinit: Resetinit: [Action, payload]带载荷运行初始 Actioninit: [SetCounter, 10]图中init之所以能直接跳转到nextState是因为在源码实现里初始化本质上就是一次 dispatch在 index.js 的app()中返回语句以(dispatch)(init)收尾见 index.js也就是说应用构造完成后的第一件事就是把init当作一个待分发值交给 dispatch 去解析。如果init是字面量状态或[state, ...effects]数组dispatch 会直接走update(action)分支完成状态设定如果init是函数则进入 Action 递归解析。三、两个持续运行的入口视图事件与订阅应用启动后维持系统运转的是流程图左侧的两条持续输入通道1. 视图事件view eventDOM/synthetic events (click/myevent) → view event → Action当用户在界面上的交互如click、input、自定义的myevent等合成事件发生时事件对象会作为默认载荷被送入对应的 Action。在 docs/architecture/views.md 中h()负责在描述 HTML 元素的同时把 actions 绑定为事件处理器。事件对象本身会成为 Action 的默认 payload因此 docs/architecture/actions.md 专门讲解了事件载荷这一话题直接使用onclick: AddBy会把整个 event 对象传入AddBy(state, amount)通常需要用[AddBy, 5]这样的**动作描述符action descriptor**预先指定载荷或用包装 Action 预处理事件如AddByValue (state, event) [AddBy, event.target.value]。2. 订阅事件subscriptionglobal/external processes (window resize) → subscription → Action订阅是应用对外部进程时间、位置、窗口尺寸变化、自定义事件等依赖关系的声明式表达见 docs/architecture/subscriptions.md。订阅一旦被激活其内部实现的**订阅者subscriber**函数就会在事件发生时手动调用dispatch把结果以 Action 的形式注入应用同时返回清理函数SubscriberFn : (DispatchFn, Payload?) - CleanupFn用于取消订阅。订阅的启停完全由状态驱动subscriptions(state)每次状态变更都会被重算index.js 中的patchSubs会逐位对比新旧订阅数组决定启动 / 关闭 / 保持详见 subscriptions.md 的生命周期表。值得一提的是无论事件来自视图还是订阅最终都汇入同一个Action节点——这正体现了 Hyperapp一切状态变更必须经由 Action的设计哲学而dispatch则是承载这一切的统一通道见 docs/architecture/dispatch.md。四、核心枢纽Action 与 dispatch 的递归流程图中Action节点有两条出边代表一个被分发的 Action 可能产生两种结果① Action → OtherAction / [OtherAction, payload?] → Action 递归 ② Action → NextState / [NextState, ...Effects] → nextState 落地递归分支Action 可以返回 Action第一种情况对应 docs/architecture/actions.md 中Action : ... | OtherAction | [OtherAction, Payload?] 的签名。一个 Action 可以返回另一个 Action甚至带载荷的动作描述符从而形成一条包装链用于逐步调整载荷或实现别名。例如const AddBy (state, amount) ({ ...state, value: state.value amount }) const AddByMore (_, amount) [AddBy, amount 5] const AddByEvenMore (_, amount) [AddByMore, amount 10] h(button, { onclick: [AddByEvenMore, 1] }, text(16))在 index.js 中这段递归由dispatch自身完成遇到函数就调用dispatch(action(state, props))继续解析遇到[函数, payload]就拆成dispatch(action[0], action[1])再次进入。正如 dispatch.md 所说dispatch 以递归方式实现——dispatch([ActionFn, payload])会递归为dispatch(ActionFn, payload)再递归为dispatch(ActionFn(currentState, payload))直到解析出真正的下一状态。落地分支产生 NextState 与 Effects第二种情况是 Action 返回新的状态或状态 副作用数组// Action : (State) - [NextState, ...Effects] const SayHi (state) [ { ...state, value: state.value 1 }, log(hi), log(there), ]这里的关键约束是返回的数组第一项必须是下一状态其余项为要运行的 Effects见 docs/architecture/effects.md 与 docs/architecture/state.md 的State With Effects一节。也正因数组有此特殊含义若想真正以数组作为状态必须再包一层[[...state, one]]。在 index.js 中dispatch 对数组的处理是update(action[0])先应用状态然后用map依次执行(fx[0] || fx)(dispatch, fx[1])运行每个效果——这正是流程图NextState / [NextState, ...Effects]连线的落地实现。效果数组中的 falsy 项会被忽略fx fx ! true ...这为state.eating log(eating)这类条件副作用写法提供了依据见 effects.md 的DoItBest示例。五、扇出点next state 同时驱动三件事一旦新的状态确定流程图右侧的nextState节点就会通过三条带标签的连线同时扇出① view(state) → (re)render DOM ② subscriptions(state) → recalc subscriptions ③ (dispatch, Payload?) → void → Effects (impure code)1. 视图重渲染view(state)调用 top-level view由app()的view:属性指定签名View : (State) - VNode得到一棵虚拟 DOMVDOM树随后 Hyperapp 执行 DOM diff/patch。在 index.js 中render通过requestAnimationFrame调度见update中的requestAnimationFrame(render, (busy true))并与浏览器原生的重绘周期保持同步patch函数index.js则负责比对新旧 VNode最小化地增删改真实 DOM支持 key 列表复用、属性增量更新、文本节点原位替换等优化。如果挂载节点已存在内容如 SSR 预渲染的 HTMLrecycleNodeindex.js还会回收这些既有 DOM 完成水合hydration。2. 订阅重算subscriptions(state)每次状态变更都会被调用返回新的订阅数组由patchSubsindex.js与上一轮的订阅逐位比对订阅首次变为 active 时启动并保存清理函数从 active 变为 inactive 时调用清理函数oldSub oldSub[2]()保持不变则原样保留。因此 subscriptions.md 强调订阅数组必须是固定大小、位置不变的动态数组或内联订阅都会因每次重算导致订阅被反复重建。3. 副作用执行(dispatch, Payload?) - void即**效果函数effecter**的签名EffecterFn : (DispatchFn, Payload?) - void见 effects.md。effecter 是应用业务逻辑与不纯代码之间的通用桥梁——它被允许使用副作用发起 HTTP 请求、操作 DOM、写 localStorage 等并在得到结果后通过手中的dispatch将结果以 Action 形式交还给应用。六、闭环Effects 通过 dispatch 把控制权交还 Action流程图最下方有一条虚线回边是理解整个架构的关键effect(Effects (impure code)) -.- |dispatch| Action它表达的是effecter 异步执行完毕后调用 dispatch 将结果 Action 注入系统从而开启新一轮Action → NextState → 三路扇出的循环。一个典型的异步 effecter 如下见 effects.mdconst runSimpleFetch async (dispatch, payload) { const response await fetch(payload.url) requestAnimationFrame(() dispatch(payload.action, response.json())) }注意这里的requestAnimationFrameeffecter 需要把回传 dispatch 的时机对齐到 Hyperapp 与浏览器同步的重绘周期上避免在渲染间隙写入状态若requestAnimationFrame不可用回退方案是setTimeout。这种发起副作用 → 异步结果经 dispatch 回到 Action → 状态更新 → 视图/订阅/新副作用再次扇出的模式构成了 Hyperapp 完整、单向、可预测的数据流。自定义订阅与自定义效果的写法完全同构订阅者同样持有一把dispatch签名SubscriberFn : (DispatchFn, Payload?) - CleanupFn事件到达时用requestAnimationFrame(() dispatch(...))回传区别仅在于订阅者额外返回清理函数负责移除监听器、关闭连接等资源回收。例如 subscriptions.md 中的listenToEvent返回() removeEventListener(props.type, listener)。七、从源码回看整条链路把整张图映射到 index.js 的app()实现可以得到一份完整的流程图 ↔ 代码对照流程图节点源码位置index.js职责initindex.js(dispatch)(init)启动首次分发dispatch 递归index.js函数/数组/字面量三种形态的解析Action → NextStateindex.jsupdate(action[0])应用新状态Effects 执行index.js(fx[0] \|\| fx)(dispatch, fx[1])view(state) 渲染index.jspatch完成 VDOM diff 与 DOM 更新subscriptions 重算index.jspatchSubs对比并启停订阅渲染/订阅节流index.jsrequestAnimationFrame对齐重绘周期还有一个重要的边界情况当 Action 返回undefined时应用会停止。在updateindex.js中一旦新状态为null/undefineddispatch、subscriptions、render会被替换为空函数随后订阅被清理、DOM 不再被触碰、事件处理器失效且已停止的应用无法重启见 actions.md 的 Stopping Your App。这也是排错时的一个线索如果应用点击无反应很可能是某个 Action 意外返回了undefined。八、总结Hyperapp 数据流的四个要点对照 flowchart.md 全图可以把 Hyperapp 的运行模型浓缩为四条规则单一入口与单一枢纽所有状态变更无论来自init、视图事件还是订阅/效果的回传都必须经过dispatch → Action这一条管道Action 是产生新状态的唯一合法途径。递归解析直至落地Action 可以返回另一个 Action或带载荷的 Action 描述符dispatch 会递归解析直到拿到NextState或[NextState, ...Effects]。状态一次扇出、驱动三路next state确定后视图重渲染、订阅重算、副作用执行三条分支并行推进互不阻塞。闭环反馈effecter / subscriber 通过手中持有的dispatch把异步结果重新注回 Action使整个系统在事件 → 状态 → 视图/订阅/副作用 → 事件的循环中持续运转同时保持状态变更的纯净性渲染由 VDOM diff 保证副作用被隔离在 effecter/subscriber 层。如果你想继续深入推荐按顺序阅读配套架构文档state → actions → effects → subscriptions → views → dispatch最后回到 app() 的 API 文档 对照每个配置项的实际用法而 index.js 中的app()、patch、patchSubs则是这张流程图最精确的实现级注释。赞分享前端【免费下载链接】hyperapp1kB-ish JavaScript framework for building hypertext applications项目地址https://gitcode.com/gh_mirrors/hy/hyperapp点击查看免费下载相关推荐FLUTE算法在Meta-Dataset上的应用学习通用模板实现数据集泛化FLUTE算法在Meta Dataset上的应用学习通用模板实现数据集泛化 Meta Dataset是一个用于小样本学习研究的数据集集合而FLUTEFewClaude Code Action与CI/CD流水线集成构建完整的开发自动化闭环Claude Code Action与CI/CD流水线集成构建完整的开发自动化闭环 在现代软件开发中CI/CD流水线已成为保障代码质量和加速交付的核心基础设AI AgentCI/CD代码智能体开发者工具SEO Machine GA4 完整实施指南从数据流、增强测量到转化、自定义维度与数据读取闭环SEO Machine GA4 完整实施指南从数据流、增强测量到转化、自定义维度与数据读取闭环 本文基于 seomachine 仓库中 analytics t人工智能AI 应用AI 写作AI 技能AI Agent上一篇pot-desktop 完整教程三步装好划词翻译与截图 OCR下一篇3分钟搞定API文件上传Bruno多部分表单配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表