
用 Ractive.js 实现 TodoMVC响应式数据绑定与极简三层结构的源码级剖析【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc在 TodoMVC 这个以“用同一份需求对比多种 JavaScript 框架”著称的仓库中Ractive.js 示例走了一条与 React、Angular、Vue 截然不同的路线它不构建 Model 类不划分多个 View 组件而是用一个 Ractive 实例 一个普通对象数组撑起整个应用。读完本篇你将理解 Ractive 的响应式reactive更新机制如何在 examples/ractive/js/app.js 中落地——包括模板内声明式事件、自定义enter/escape事件、计算属性、数组变更拦截、基于localStorage的持久化以及用 Director 路由驱动过滤器切换的完整调用链。为什么 Ractive 示例里没有 Model 层Ractive.js 示例的官方说明开宗明义Ractive.js 并不是传统意义上的 MVC 框架——“没有 Model 类只有一个普通的任务对象数组以及唯一的一个视图对象整个应用存在于单个文件中另有两个小文件分别负责持久化和路由”。这与很多示例形成鲜明对比examples/backbone 或 examples/jquery 目录下都会看到独立的models/、collections/、views/目录而 examples/ractive 目录只有三个 JS 文件文件职责js/app.js创建唯一的 Ractive 实例、定义模板事件处理逻辑相当于整个应用js/persistence.js从localStorage读取/回写任务列表js/routes.js基于 Director 的路由切换currentFilterREADME 进一步解释了这种设计取向Ractive 最初是为在 theguardian.com 上开发互动新闻应用而生的需要在苛刻的截稿压力下快速迭代因此“为开发者的 sanity 做了优化”。它拥抱响应式编程原则API 直白到“5 分钟能学会几小时能掌握”兼具 Backbone 类库的灵活性与 Angular 类框架的能力并带有动画、声明式过渡、一等公民的 SVG 支持、模板中逻辑表达式配合精细依赖追踪等特性。应用入口index.html 中的声明式模板index.html 是整个应用的骨架。它通过html>link relstylesheet hrefnode_modules/todomvc-common/base.css link relstylesheet hrefnode_modules/todomvc-app-css/index.css link relstylesheet hrefcss/app.css其中 css/app.css 只有短短 10 行去掉 checkbox 的 outline并禁止label被选中——这是为了双击编辑时不产生选中高亮的视觉干扰。依赖脚本按顺序加载版本由 package.json 锁定{ private: true, dependencies: { director: ^1.2.8, ractive: ^0.3.5, todomvc-app-css: ^2.1.0, todomvc-common: ^1.0.1 } }script srcnode_modules/todomvc-common/base.js/script script srcnode_modules/ractive/build/Ractive.js/script script srcnode_modules/director/build/director.js/script script srcjs/app.js/script script srcjs/persistence.js/script script srcjs/routes.js/script注意顺序app.js先创建实例并挂到window.todoList随后persistence.js和routes.js直接复用这个全局实例——三个文件通过全局变量协作没有任何模块系统。模板放在typetext/ractive的 script 标签里分成#main主模板和#item子模板partial。几个关键写法值得细读!-- 自定义 enter 事件回车时提交新任务 -- input classnew-todo on-enternewTodo placeholderWhat needs to be done? autofocus !-- 只有 items.length 为真值时才渲染主体与页脚 -- {{#items.length}} section classmain !-- 勾选框状态由逻辑表达式推导响应式更新无需手动刷新 -- input idtoggle-all typecheckbox on-changetoggleAll checked{{ items.length completedTasks().length }} ul classtodo-list {{#items:i}} {{item}} {{/items}} /ul /section ... !-- 只有存在已完成任务时才渲染清除按钮 -- {{# completedTasks().length }} button classclear-completed on-clickclearCompletedClear completed/button {{/ end of filter }} {{/items.length}}!-- #item partial每个任务渲染一次this 指向当前任务对象 -- {{# filter(this) }} li class{{ completed ? completed : }} {{ editing ? editing : }} div classview input classtoggle typecheckbox checked{{completed}} label on-dblclickedit{{description}}/label button on-clickremove:{{i}} classdestroy/button /div !-- 仅在编辑状态存在于 DOM 中 -- {{#.editing}} input idedit classedit on-blur-entersubmit on-escapecancel autofocus {{/.editing}} /li {{/ end of filter }}这里体现了 README 所说的“模板内逻辑表达式”{{# filter(this) }}是带函数调用的逻辑表达式任务通过当前过滤器时才渲染on-blur-entersubmit把标准 DOM 事件blur与自定义事件enter组合在同一个触发器上on-clickremove:{{i}}则把循环索引作为参数传给事件处理器。核心实现app.js 中的 Ractive 实例js/app.js 是整个应用的“唯一视图对象”。实例创建代码如下对应 app.jsvar todoList new Ractive({ el: .todoapp, // 目标元素ID、CSS 选择器或元素本身 template: #main, // 模板#main script 标签 data: { filter: function (item) { // this.get(currentFilter) 让 Ractive 建立依赖追踪 // currentFilter 变化时该函数会被重新求值 var currentFilter this.get(currentFilter); if (currentFilter all) { return true; } return filterscurrentFilter; }, // 计算属性items 变化含 items[1].completed 这类子属性变化时 // 自动重算并驱动视图更新 completedTasks: function () { return this.get(items).filter(filters.completed); }, activeTasks: function () { return this.get(items).filter(filters.active); }, currentFilter: all // 路由切换时改变此值 }, ... });几个值得注意的实现细节依赖追踪是显式 API 驱动的。filter内部用this.get(currentFilter)而非闭包变量Ractive 正是靠这条get调用知道该函数依赖currentFilter从而在其变化时重新执行。这是模板里{{# filter(this) }}能响应路由切换的底层原因。计算属性即模板可调用函数。completedTasks()、activeTasks()定义在data里模板中像{{ activeTasks().length }}这样调用items数组任何变化都会触发重算——页脚里的计数因此“自动”保持最新没有任何手动render()调用。自定义事件机制见 app.jsenter和escape不是 DOM 原生事件而是通过events配置注入的。每个自定义事件是一个(node, fire)工厂函数内部监听keydown按键码匹配ENTER_KEY 13、ESCAPE_KEY 27时调用fire({ node, original })并返回带teardown的清理对象以便节点移除时注销监听。这就是模板里on-enternewTodo、on-escapecancel能工作的全部原理。事件处理器通过todoList.on(...)集中定义app.js与 Ractive“事件语义写进模板”的设计呼应——不像一些库用外部 CSS 选择器映射事件remove(event, index)this.get(items).splice(index, 1)。注释点破了 Ractive 的关键机制Ractive 拦截数组的突变方法如splice、push并以最高效的方式更新 DOM无需重建列表。newTodo(event)从event.node.value取输入、trim()后空值直接返回非空则push({ description, completed: false })再清空输入框。edit(event)用this.set(event.keypath .editing, true)按 keypath 精确写入单个任务的编辑状态并把描述回填进this.nodes.edit输入框。event.keypath形如items[2]与event.index.i是事件对象提供的上下文定位符避免了“查哪个元素正在被编辑”的额外状态。submit(event)编辑提交。空描述则splice(event.index.i, 1)删除该任务符合 app-spec.md 中“空值编辑即删除”的约定否则set(keypath .description, ...)并退出编辑态。toggleAll(event)最讲究的一处性能细节。它遍历所有任务构建changeHash一次性this.set(changeHash)批量提交所有items[i].completed变更而不是循环调用set造成多次渲染。clearCompleted()倒序遍历数组splice掉已完成项倒序是为了避免删除导致的索引偏移。最后window.todoList todoList;把实例暴露为全局变量供后两个文件使用——这也解释了为什么三个文件能如此小巧地协作。持久化persistence.js 的响应式回写js/persistence.js 印证了 README 中“模型通常就是 POJOplain old JavaScript objects”的说法——持久化的就是一个对象数组// Firefox 在禁用 cookie 时访问 localStorage 会抛 SecurityError try { localStorage window.localStorage; } catch (err) { todoList.set(items, []); return; } if (localStorage) { items JSON.parse(localStorage.getItem(todos-ractive)) || []; // 编辑态editing不应持久化 removeEditingState function (item) { return { description: item.description, completed: item.completed }; }; // 模型任何变化含 items[1].completed 这类子属性都触发回写 todoList.observe(items, function (items) { localStorage.setItem(todos-ractive, JSON.stringify(items.map(removeEditingState))); }); } todoList.set(items, items);三个设计点存储键为todos-ractive与其他框架示例如todos-backbone隔离避免同一浏览器里互相污染。todoList.observe(items, ...)是响应式持久化的核心不用在每个事件处理器里手动save()而是声明式地观察数据路径任何深层变更都会触发回调。removeEditingState剥离编辑态再序列化注释明确引用了 app-spec.md 的 Persistence 规范——editing只是 UI 状态不应跨会话保留。对localStorage不可用的场景做了两级降级访问抛错如 Firefox 禁用 cookie或不存在时都以空数组启动保证应用仍可运行。路由routes.js 只用一个 data 属性桥接js/routes.js 不到 25 行使用 Director 路由库var router new Router({ /active: function () { todoList.set(currentFilter, active); }, /completed: function () { todoList.set(currentFilter, completed); } }); router.configure({ notfound: function () { window.location.hash ; todoList.set(currentFilter, all); } }); router.init();这里能清晰看到整条响应式数据流URL hash#/active→ 路由回调 →set(currentFilter, ...)→filter(this)因依赖追踪重新求值 → 模板中{{# filter(this) }}重新判定每个li的显隐。模板里的三个筛选链接index.html分别指向#/、#/active、#/completed选中态同样由逻辑表达式class{{ currentFilter all ? selected : }}推导而不是手动切换 class。未匹配路由时回退到all并清空 hash与 app-spec.md 的路由行为保持一致。学习资源与延伸阅读examples/ractive/readme.md 还给出了学习路径官方提供 60 秒上手指南60 Second Setup与交互式教程learn.ractivejs.orgAPI 文档位于 docs.ractivejs.org问题可在 Stack Overflow 的ractivejs标签下讨论。该 TodoMVC 实现由 Rich Harris 完成。如果你想横向对比不同框架对同一规范的处理本仓库还有现成的参照物examples/backbone/readme.md 展示了传统三层 Model/View/Collection 结构examples/jquery/readme.md 则是手写 DOM 操作的基线版本而 tests/ 目录与 cypress/e2e/spec.cy.js 定义了所有示例必须通过的统一验收测试——其中持久化与编辑态的处理正是上文removeEditingState遵循的依据。小结这套实现的“最小响应式”范式Ractive 版 TodoMVC 的价值不在于功能多少而在于它用最少代码演示了一条完整的响应式链路且每一环都能在源码中定位单一实例 POJO 数组替代传统 MVC 分层状态即data模板声明事件on-enter、on-blur-enter、remove:{{i}}语义不落在外部选择器上get调用建立依赖追踪让filter(this)、计算属性对数据变化自动重算数组突变方法被拦截push/splice直接驱动最小化 DOM 更新observe(items, ...)一行代码完成持久化回写且剥离 UI 态路由只改一个 data 属性UI 全链路由响应式机制推导。理解这套写法后阅读仓库中其他响应式框架示例如 Vue、Preact、Svelte会更容易抽象出共性框架差异主要体现在“依赖如何被追踪”与“更新如何被调度”而 TodoMVC 这份需求则始终作为不变量贯穿其中。【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考