资讯动态

Hyperapp `app()` 完全指南:初始化、挂载、订阅与自定义分发机制解析

发布时间:2026/9/20 12:54:06 来源:尧图企业网站定制
Hyperappapp()完全指南初始化、挂载、订阅与自定义分发机制解析【免费下载链接】hyperapp1kB-ish JavaScript framework for building hypertext applications项目地址: https://gitcode.com/gh_mirrors/hy/hyperappapp()是 Hyperapp 唯一的应用启动入口它接收配置对象完成初始状态设置、订阅注册与虚拟 DOM 挂载并返回应用自身使用的 dispatch 函数。本文以 docs/api/app.md 为骨架逐项解析init、view、node、subscriptions、dispatch五个配置项的全部形态与默认行为并结合入口源码 index.js 与架构文档讲清楚调用app()之后到底发生了什么以及如何用返回值实现外部控制、资源释放和多应用共存。读完你就能独立完成一个 Hyperapp 应用的最小搭建、状态驱动的渲染、订阅管理以及基于自定义 dispatch 的调试与中间件扩展。app()是什么一切应用的启动入口Hyperapp 本身只有一个核心 API 函数就是app()。它的职责是初始化并挂载一个 Hyperapp 应用——设置初始状态、渲染首次视图、注册订阅并建立贯穿应用生命周期的 dispatch 通道。文档给出的类型签名如下Elm 风格app : ({ Init, View, Node, Subscriptions?, Dispatch? }) - DispatchFn即app()接收一个包含Init、View、Node三个必需/条件必需项以及Subscriptions、Dispatch两个可选项的配置对象返回一个DispatchFndispatch 函数。配置项总览PropTypeRequired?init:State[State, ...Effect[]]Action[Action, any]Noview:ViewNonode:DOM elementYes whenview:is present.subscriptions:FunctionNodispatch:Dispatch InitializerNo其中node的约束值得注意只要传入了view就必须同时提供node因为 Hyperapp 需要一个真实的 DOM 元素作为渲染目标。返回值Return ValueTypedispatchFunction最小可运行示例import { app, h, text } from hyperapp app({ init: { message: Hello World! }, view: (state) h(p, {}, text(state.message)), node: document.getElementById(app), })这段代码完成了 Hyperapp 应用的最小闭环init给出初始状态view把状态映射为虚拟 DOMh与text的用法见 docs/api/h.md 与 docs/api/text.mdnode指定挂载点。状态、视图、动作、效果、订阅这五大概念如何协同工作可参阅 docs/tutorial.md 与 docs/reference.md。源码入口从源码结构看app()的全部逻辑都集中在入口文件的单个函数中index.js#L363-L414。它同时定义了参数默认值、dispatch 核心逻辑、update/render渲染链路和patchSubs订阅协调是本仓库最核心的实现文件。后面的每个小节都会回到这里做源码级印证。init:初始状态与初始动作init的默认值是{}空对象对应源码 index.js#L368 中的init EMPTY_OBJ。它用于在应用启动时设置 state 的初始值或执行一个初始 action。文档明确其执行时机发生在首次视图渲染与订阅注册之前。也就是说init中产生的状态与效果会在用户看到第一帧 UI、任何订阅启动之前就被处理完毕。init:的四种形态1.init: state—— 直接设置初始状态app({ init: { counter: 0 }, // ... })2.init: [state, ...effects]—— 设置初始状态并运行效果数组的第一个元素是初始状态其余元素是要运行的 effects如日志、HTTP 请求、DOM 操作等app({ init: [ { loading: true }, log(Loading...), load(myUrl?init, DoneAction), ], // ... })这里log、load都是效果创建函数load在请求完成时会把结果作为 payload 派发给DoneAction。关于状态与效果并存数组的语义详见 state.md数组形式的返回值被 Hyperapp 解释为特殊数组第一项是状态后续项是需要运行的效果且先应用状态、再按顺序执行效果。3.init: Action—— 运行一个动作直接传入一个 actionconst Reset (_state) ({ counter: 0 }) app({ init: Reset, // ... })这种形式在动作需要被后续复用时非常有用例如Reset之后还能绑到按钮的onclick上。注意一个细节此场景下传入动作的 state 是undefined所以动作实现里通常忽略第一个参数。4.init: [Action, payload]—— 带负载运行动作使用动作描述符action descriptor给初始动作携带一个 payloadconst SetCounter (_state, n) ({ counter: n }) app({ init: [SetCounter, 10], // ... })源码印证init如何被消费在 index.js#L411 中app()最终以dispatch(init)启动整个分发链。由于 dispatch 对函数 / 数组 / 字面量三种输入做了统一归一化见下文启动流程源码走读四种init形态才能被同一套机制处理init是字面量状态 → 直接update(action)设置状态init是数组且首项非函数 → 解释为[state, ...effects]init是函数 → 视为动作以undefined为初始 state 调用init是[Action, payload]→ 递归为dispatch(Action, payload)。view:顶层视图view定义应用的顶层视图top-level view——即代表整个应用的那个视图函数。一个应用有且仅有一个顶层视图Hyperapp 用它把 state 映射成 UI。关键行为每次应用状态发生变化view都会被重新调用根据新状态重新生成虚拟 DOMHyperapp 再通过 diff 算法高效更新真实 DOM见 views.md。app({ // ... view: (state) h(main, {}, [ outworld(state), netherrealm(state), ]), })视图天然可组合上面的outworld、netherrealm都是接收 state 的组件components它们可以是子视图也可以是任何返回 VNode 的函数。源码印证渲染时机与节流在 index.js#L375-L391 中update与render共同实现了状态变化 → 重渲染链路var update (newState) { if (state ! newState) { if ((state newState) null) dispatch subscriptions render id if (subscriptions) subs patchSubs(subs, subscriptions(state), dispatch) if (view !busy) requestAnimationFrame(render, (busy true)) } } var render () (node patch( node.parentNode, node, vdom, (vdom view(state)), listener, (busy false) ))两个值得注意的实现细节state ! newState的引用比较只有新旧状态引用不同才会触发渲染。这正是 state.md 中状态必须保持不可变、每次返回新对象的原因——如果直接修改并返回原对象update会认为没有变化而什么都不做。requestAnimationFrame节流busy标志保证同一帧内多次状态变化只触发一次渲染渲染与浏览器的自然重绘周期同步requestAnimationFrame的用法在 effects.md 中也有强调。node:挂载节点与挂载Mountingnode是应用要渲染到的真实 DOM 元素称为挂载节点。Hyperapp 会用虚拟 DOM 渲染结果替换该元素这个过程就是挂载mounting。惯例做法是在 HTML 中定义一个有意保持空白的元素并给它一个应用可以引用的 IDmain idapp/mainapp({ // ... node: document.getElementById(app), })回收RecyclingSSR 与预渲染的关键文档特别强调如果挂载节点内已有内容Hyperapp 会尝试回收recycle这些内容而不是丢弃重建。这为服务端渲染SSR或预渲染提供了天然支持——服务器/构建期先输出完整 HTML浏览器端app()挂载时直接复用现有 DOM从而获得 SEO 与首屏性能收益。完整的回收语义见 views.md。源码印证recycleNode挂载时的回收逻辑在 index.js#L331-L340var recycleNode (node) node.nodeType TEXT_NODE ? text(node.nodeValue, node) : createVNode( node.nodeName.toLowerCase(), EMPTY_OBJ, map.call(node.childNodes, recycleNode), SSR_NODE, node )app()启动时若提供了node会先执行recycleNode(node)index.js#L370把挂载节点现有的 DOM 树递归地转换为虚拟 DOM文本节点转成textVNode元素节点转成带SSR_NODE标记的 VNode随后render通过patch在新旧 VNode 间做差异化更新从而复用已有节点。patch中的oldVNode.type SSR_NODE判断index.js#L247正是为这种服务端预渲染内容场景准备的。subscriptions:订阅管理subscriptions是一个函数接收当前状态返回一个 subscriptions 数组。每次应用状态变化这个函数都会被调用以重新确定当前应激活哪些订阅。import { onKey } from ./subs app({ // ... subscriptions: (state) [ onKey(w, MoveForward), onKey(a, MoveBackward), onKey(s, StrafeLeft), onKey(d, StrafeRight), state.playingDOOM1993 || onKey( , Jump), ], })两个重要行为falsy 条目即退订如果某个数组位置上是 falsy 值false、null、undefined、0等那么该位置上原本的订阅会被视为已退订并执行清理。上面的例子利用||实现了条件订阅——只有state.playingDOOM1993为真时才订阅空格键跳跃。省略即无订阅如果省略subscriptions应用没有任何订阅等价于subscriptions: (state) []。订阅的生命周期订阅的启停由 Hyperapp 在每次状态变化时自动协调subscriptions.md 给出了完整的判定表之前激活当前激活发生什么否否什么都不做。否是订阅启动。是否订阅关闭并执行清理。是是订阅保持激活。注意订阅数组格式的限制subscriptions.md数组大小需固定每个位置要么是布尔值、要么是固定在同一位置的订阅动态长度数组和行内匿名订阅都不会正常工作。源码印证patchSubs订阅的差异对比与启停由 index.js#L39-L63 的patchSubs实现并在每次状态更新时被update调用index.js#L378var patchSubs (oldSubs, newSubs EMPTY_ARR, dispatch) { for ( var subs [], i 0, oldSub, newSub; i oldSubs.length || i newSubs.length; i ) { oldSub oldSubs[i] newSub newSubs[i] subs.push( newSub newSub ! true ? !oldSub || newSub[0] ! oldSub[0] || shouldRestart(newSub[1], oldSub[1]) ? [ newSub[0], newSub[1], (oldSub oldSub[2](), newSub0), ] : oldSub : oldSub oldSub[2]() ) } return subs }从中可以看到每个位置上的订阅如果状态变化订阅函数引用不同或 payload 经shouldRestart判定需要重启旧订阅的清理函数oldSub[2]()会先执行随后用新的订阅函数和 payload 启动新订阅。若某个位置变成 falsy则只执行旧订阅的清理实现退订。dispatch:自定义分发dispatch是一个分发初始化器dispatch initializer它接收默认的 dispatch 作为唯一参数并必须返回一个 dispatch 供应用使用。它允许你创建自定义 dispatch 函数来替代默认实现从而切入分发过程做调试、测试、遥测等类似其他框架的中间件概念见 dispatch.md。Hyperapp 的默认分发初始化器等价于const boring (dispatch) dispatch自定义初始化器通常返回常规 dispatch 的一个变体app({ // ... dispatch: logActionsMiddleware, })源码印证默认值与调用时机在 index.js#L367 中dispatch参数的默认值就是恒等函数idvar id (a) aindex.js#L7与文档中的boring等价。从 index.js#L397-L411 可以确认两个关键事实初始化器只被调用一次app()在实例化过程中把默认 dispatch 传入初始化器得到的自定义 dispatch 立即被用于派发init之后整个应用生命周期都用这个 dispatch。一个应用只有一个 dispatch每个应用只能定义一个初始化器因此也只能有一个 dispatchdispatch.md 中明确说明。中间件示例日志与不可变状态利用自定义 dispatch 做调试非常方便。记录每次派发的动作dispatch.mdconst logActionsMiddleware dispatch (action, payload) { if (typeof action function) { console.log(DISPATCH: , action.name || action) } // pass on to original dispatch dispatch(action, payload) }记录每次状态变换dispatch.mdconst stateMiddleware fn dispatch (action, payload) { if (Array.isArray(action) typeof action[0] ! function) { action [fn(action[0]), ...action.slice(1)] } else if (!Array.isArray(action) typeof action ! function) { action fn(action) } dispatch(action, payload) } const logStateMiddleware stateMiddleware(state { console.log(STATE:, state) return state })多个中间件可以链式组合顺序从左到右逐层包裹dispatch.mdimport { logActionsMiddleware, logStateMiddleware, immutableMiddleware } from ./middleware.js app({ // ... dispatch: dispatch logStateMiddleware(logActionsMiddleware(immutableMiddleware(dispatch))) })需要提醒的是dispatch 的分发本质是递归的dispatch.md。dispatch([ActionFn, payload])会递归到dispatch(ActionFn, payload)再递归到dispatch(ActionFn(currentState, payload))直到解析出最终的下一状态。自定义 dispatch 必须调用原始 dispatch 才能让这条递归链继续下去。返回值外部控制与停止应用app()返回应用所使用的dispatch 函数。这在你的应用只有一部分由 Hyperapp 实现、其余部分由外部代码控制的场景下非常有用——你可以把返回的 dispatch 暴露给外部让外部代码随时派发动作。更关键的是停止机制无参数调用返回的 dispatch会释放应用资源并运行所有激活订阅的清理函数。这等价于把状态过渡到undefined见 actions.md 的Stop动作之后订阅全部停止、DOM 不再被触碰、事件处理器失效且已停止的应用无法重启。源码印证停止逻辑在 index.js#L377 中if ((state newState) null) dispatch subscriptions render id当派发的结果是null或undefined时dispatch、subscriptions、render被一并替换为恒等函数——分发通道被断开订阅协调和渲染都不再执行应用停止。这也是为什么 state.md 提醒动作什么都不返回时应用会停止。外部控制示例// 把 dispatch 暴露给应用外部 export const myApp app({ init: { count: 0 }, view: (state) h(p, {}, text(Count: ${state.count})), node: document.getElementById(app), }) // 外部代码直接派发状态 myApp({ count: 42 }) // 或者派发动作 myApp((state) ({ ...state, count: state.count 1 })) // 停止应用并清理订阅 myApp()其他考量嵌入与多实例共存嵌入其他应用你可以把 Hyperapp 应用嵌入到另一个已存在的 Hyperapp 应用或嵌入到用其他框架构建的应用中例如在 subscriptions.md 里展示的嵌入遗留原生 JavaScript 项目模式——把app()包在一个导出函数里供外层调用并通过自定义事件订阅与宿主通信。多应用并存同一页面可以同时存在多个 Hyperapp 应用每个应用拥有独立的状态、独立运行、互不干扰它们之间可以通过订阅与效果例如自定义事件通信。由于每个应用在app()内部都持有自己独立的state、vdom、subs闭包index.js#L370-L373多实例之间天然隔离。启动流程源码走读一次app()调用发生了什么最后把整条调用链串起来对应 index.js#L363-L414app()接收配置对象取出node、view、subscriptions、dispatch、init其中dispatch默认id、init默认{}若提供了node先用recycleNode把挂载节点的现有 DOM 回收为虚拟 DOM支持 SSR 预渲染内容调用dispatch初始化器把默认 dispatch 换成自定义版本立即dispatch(init)启动应用进入递归分发字面量状态直接update函数动作以当前状态执行[state, ...effects]先更新状态再逐个运行效果update触发订阅协调patchSubs与渲染调度requestAnimationFrame→render→patch此后每一次 DOM 事件、效果、订阅派发动作都走同一条 dispatch → update → 订阅协调 渲染 的回路直到 dispatch 被无参数调用或状态过渡到undefined使应用停止。进一步阅读五个架构概念state、actions、effects、subscriptions、views、dispatch视图构建 APIh、text、memo上手教程docs/tutorial.md完整参考docs/reference.md官方扩展包DOM、SVG、HTML、time、events 等见 packages/dom、packages/svg、packages/html、packages/time、packages/events【免费下载链接】hyperapp1kB-ish JavaScript framework for building hypertext applications项目地址: https://gitcode.com/gh_mirrors/hy/hyperapp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价