Hyperapp 是最近热度颇高的一款迷你 JS 框架,其源码不到 400 行,压缩 gzip 后只有 1kB,却具有相当高的完成度,拿来实现简单的 web 应用也不在话下。整体实现上,Hyperapp 的思路与 React 比较类似,都是借助 Virtual DOM 来实现高效的 DOM 更新。在探究 Hyperapp 背后的实现原理之前,我们先看一下如何使用它。

注:本文基于 Hyperapp 1.2.5 版本。

使用

官方的文档中给出了一个示例应用(在线 demo 点我),代码如下:

import { h, app } from "hyperapp"const state = {count: 0
}const actions = {down: value => state => ({ count: state.count - value }),up: value => state => ({ count: state.count + value })
}const view = (state, actions) => (<div><h1>{state.count}</h1><button onclick={() => actions.down(1)}>-</button><button onclick={() => actions.up(1)}>+</button></div>
)app(state, actions, view, document.body)

几点简单的说明帮助你快速上手 Hyperapp:

  • state 用于保存整个应用的数据,其无法直接修改
  • 只有 actions 中的方法能够修改 state 中的数据
  • state 中的数据修改后,视图会自动进行更新
  • view 函数生成应用的视图,可以使用 JSX 语法

首先,Hyperapp 对外只暴露两个函数:happ。其中 app 用于将应用挂载到 DOM 节点上,相当于启动函数。而 h 则用于处理 view,返回 Virtual DOM 节点。由于浏览器并不能理解上面示例中 view 函数使用的 JSX 语法,因此需要通过 Babel 等编译工具进行处理(React 党应该对这些比较熟悉)。安装 transform-react-jsx 插件后,在 .babel.rc 中指定该插件,同时将 pragma 设置为 h

{"plugins": [["transform-react-jsx", { "pragma": "h" }]]
}

如此,经过 Babel 编译后,上面的 view 函数就变成了如下这样:

const view = (state, actions) =>h("div", {}, [h("h1", {}, state.count),h("button", { onclick: () => actions.down(1) }, "-"),h("button", { onclick: () => actions.up(1) }, "+")])

我们的 h 函数一顿操作后,返回的 Virtual DOM 节点的结构长这样:

{nodeName: "div",attributes: {},children: [{nodeName: "h1",attributes: {},children: [0]},{nodeName: "button",attributes: { ... },children: ["-"]},{nodeName:   "button",attributes: { ... },children: ["+"]}]
}

说白了 Virtual DOM 听起来高大上,实际上就是用 JavaScript 中的 Object 数据类型去描述一个DOM 节点,因为保存在内存中,所以更新修改很快,同时加上一些 diff 算法的优化,能够最大程度地降低 DOM 节点的渲染耗费。

当然,Hyperapp 也支持 @hyperapp/html, hyperx 等其他可以生成 Virtual DOM 的库,此处不表。

源码解析

回到源码上来,由于 Hyperapp 所有的操作都在 app 函数中完成,下面就来探究一下 app 函数都做了什么。该函数主流程相当简单,源码总计十来行,先贴在下面,后面慢慢分析:

export function app(state, actions, view, container) {var map = [].mapvar rootElement = (container && container.children[0]) || nullvar oldNode = rootElement && recycleElement(rootElement)var lifecycle = []var skipRendervar isRecycling = truevar globalState = clone(state)var wiredActions = wireStateToActions([], globalState, clone(actions))scheduleRender()return wiredActions
}

生命周期

首先我们先从整体来看一下 Hyperapp 在调用 app 函数启动应用后的生命周期,如下图所示:

当然,这只是一个相当粗略的生命周期示意,但我们也能从中了解到 Hyperapp 本身相对简单的结构(对一个迷你框架来说,内部也不会复杂到哪去)。简单解释一下上图中几个函数的实现。

app 函数执行后,经过一系列准备动作后,会调用 scheduleRender 函数进行视图渲染。顾名思义,该函数是调度渲染的意思。我们看一下源码:

  function scheduleRender() {if (!skipRender) {skipRender = truesetTimeout(render)}}

可以看到,实际执行渲染的操作交由 render 函数来处理,执行的时机由 setTimeout(function(){}, 0) 决定,也就是下一个 event loop 开始后,是异步进行的。而这里 skipRender 是一个锁变量,保证在每一个 event loop 中 state 无论有多少次改变只会进行一次渲染。想象一下这样一个场景:我们在一个循环中执行了 1000 次 actions 中的某个方法来改变 state 中的值,如果不进行以上的操作,那么视图会渲染 1000 次,相当消耗性能,而这是非常不合理的。实际上 Hyperapp 的处理也略显粗糙,在更为复杂的前端框架中,会有非常完备的方案,比如 Vue 的 $nextTick 实现就复杂许多,详情可以参考这篇文章——Vue nextTick 机制。

render 调用 resolveNode 以获取最新的 Virtual DOM 形式的节点,再交由 patch 函数进行新旧节点的对比然后更新视图,同时把新节点的值赋给旧节点,方便下次比较更新。除了在最后 patch 更新视图时会进行 DOM 操作,其他时候,节点都是以 Virtual DOM 形式保存于内存中,只要新旧节点的 diff 算法足够高效,就能保持较高的视图更新效率。

除了初始化时的渲染之外,每当 actions 中的方法修改了 state 中的数据时,也会触发渲染。当然,Hyperapp 并没有去 “observe” state,而是通过对 actions 中的方法进行包装实现了这个功能(这也是 Hyperapp 规定只有 actions 中的方法能够修改 state 中的数据的原因)。

actions 处理

下面就来看一下 Hyperapp 如何对 actions 中的方法进行处理以使其在调用后能够触发 scheduleRender 的。app 函数执行初次渲染之前的准备工作里,最重要的操作就是处理 actions 中的方法。在研究其源码前,我们先看一下 Hyperapp 对 actions 中的方法制定的规范,当 state 中无嵌套对象时,总结起来大致是以下几条:

  • 必须是一元函数(只接受一个参数)
  • 函数返回值必须是以下几种:

    • “a partial state object”,也就是包含 state 中部分状态的 object。新的 state 将是原有的 state 与该返回值的浅合并(shallow merge)。例如:
      const state = {name: 'chris',age: 20}const actions = {setAge: newAge => ({ age: newAge })}
    • 一个接受当前 stateactions 为参数的函数,该函数的返回值必须为“a partial state object”。注意此时不能将接受的 state 参数直接修改后返回。正确的示例如下:
      const actions = {down: value => state => ({ count: state.count - value }),up: value => state => ({ count: state.count + value })}
    • Promise/null/undefined。此时将不会触发视图的重新渲染。

state 中有嵌套对象时,actions 中对应的属性值为一个 partial state object,其实本质上没有区别,看下面的示例应该就能理解:

const state = {counter: {count: 0}
}const actions = {counter: {down: value => state => ({ count: state.count - value }),up: value => state => ({ count: state.count + value })}
}

现在我们来看一下 Hyperapp 对 actions 中方法的处理:

  /*** * @param {Array} path  储存 state 中每层的 key,用于获取和设置 partial state object* @param {Object} state * @param {Object} actions */function wireStateToActions(path, state, actions) {// 遍历 actionsfor (var key in actions) {typeof actions[key] === "function"// actions 中属性值为函数时,重新封装? (function(key, action) {actions[key] = function(data) {// 执行方法var result = action(data)/*返回值是函数时,传入 state 和 actions 再次执行之得到 partial state object*/if (typeof result === "function") {result = result(getPartialState(path, globalState), actions)}/* result 不是 Promise/null/undefined意味着 result 返回的是 partial state object同时 result 与当前的 globalState(保存在全局的 state 的副本)中的 partial state object 不一致时 调用 scheduleRender 重新渲染视图*/if (result &&result !== (state = getPartialState(path, globalState)) &&!result.then // !isPromise) {// globalState 立即更新// 安排视图渲染scheduleRender((globalState = setPartialState(path,clone(state, result),globalState)))}return result}})(key, actions[key])// 直接返回 partial state object : wireStateToActions(// 当 state 有嵌套时,规范要求 actions 中也有相同的嵌套层级path.concat(key),(state[key] = clone(state[key])),(actions[key] = clone(actions[key])))}// 返回处理之后的所有函数// 作为对外接口return actions}

注释已经说的比较详细,总结一下就是 Hyperapp 把 actions 中的所有方法遍历了一遍,在其执行完对 state 中数据的“修改”后,调用 scheduleRender 重新渲染视图。这里之所以给“修改”打上引号,是因为实际上 actions 并没有真的去修改 state 中数据的值,而是每次用一个新的 object 去替换了 state。这里涉及到一个 “Immutability” 的概念,也就是不可变性。这种特性使得我们可以像时光穿梭一般去调试代码(因为每一步操作的 state 都保存在内存中,类似快照一般)。这也是为什么上面的代码中我们可以直接用 === 去比较两个 object 的原因。

Virtual DOM

继续顺着生命周期看下去,在页面渲染开始前,Hyperapp 会将初始化时传入 app 函数的根节点以及 view 函数生成的节点全部处理为 Virtual DOM,其形式如文章开头第一节所示。在此基础上,Hyperapp 提供了 createElement/updateElement/removeElement/removeChildren/updateAttribute 等方法,用于处理从 Virtual DOM 到真实 DOM 节点的映射。

新旧节点 diff 更新

下面就是最关键的节点更新的部分了。可以说,diff 更新是决定类 React 框架性能最重要的部分。我们来看 Hyperapp 是如何做的。新旧节点的 diff 和更新都由 patch 函数完成。其接受以下 4 个参数(实际为 5 个,第 5 个参数为 svg 相关,此处暂不讨论):parent(当前层级根节点的父节点,DOM 节点)、element(当前层级的根节点,DOM 节点,初始由 oldNode 映射生成)、oldNode(Virtual DOM)、newNode(Virtual DOM)。patch 函数根据新旧节点的不同可以按照先后优先级进行以下四种操作:

  1. 新旧节点相同(可直接通过 === 判断)时:不进行任何操作,直接返回
  2. 旧节点不存在或者新旧节点不同(通过 nodeName 判断)时:
    调用 createElement 创建新节点,并插入到 parent 的子元素中。如果旧节点存在,调用 removeElement 删除之。
  3. 新旧节点均为非元素节点时:
    elementnodeValue 值赋为 newNode。根据 DOM Level 2 规范,除 text,comment,CDATA 和 attribute 节点之外的其他类型节点,其 nodeValue 均为 null。而对于以上四种节点,直接更新其 nodeValue 值即可完成节点更新
  4. 新旧节点均存在,同时节点名称相同(即新旧节点 nodeName 相同但二者不是同一节点,区别于情况一):
    逻辑上是先更新节点属性,然后进入 children 数组中递归调用 patch 函数进行更新。不过 Hyperapp 为了提高性能,为节点提供了 key 属性。拥有 key 属性的 Virtual DOM 将对应特定的 DOM 节点(每个节点的 key 属性值需要保证在兄弟节点中中唯一 )。这样在更新时可以直接将其插入到新的位置,而不用低效率地删除再新建节点。下面的流程图说明了这里的策略:

Hyperapp 是一个很有意思的框架,除了以上分析的特点,借助 JSX 其还实现了组件化、组件懒加载、子组件插槽、节点生命周期钩子函数等高级特性。项目地址在此,大家可以自行查看学习。

本文首发于我的博客(点此查看),欢迎关注。

迷你 JS 框架 Hyperapp 源码解析相关推荐

  1. 深度学习框架Caffe源码解析

    作者:薛云峰(https://github.com/HolidayXue),主要从事视频图像算法的研究, 本文来源微信公众号:深度学习大讲堂.  原文:深度学习框架Caffe源码解析  欢迎技术投稿. ...

  2. Java熔断框架有哪些_降级熔断框架 Hystrix 源码解析:滑动窗口统计

    降级熔断框架 Hystrix 源码解析:滑动窗口统计 概述 Hystrix 是一个开源的降级熔断框架,用于提高服务可靠性,适用于依赖大量外部服务的业务系统.什么是降级熔断呢? 降级 业务降级,是指牺牲 ...

  3. 【开源项目】动态线程池框架Hippo4j源码解析

    动态线程池框架Hippo4j源码解析 项目简介 Hippo-4J 通过对 JDK 线程池增强,以及扩展三方框架底层线程池等功能,为业务系统提高线上运行保障能力. 快速开始 https://hippo4 ...

  4. react相关代码库以及框架的源码解析

    持续更新中react相关库源码浅析, react ts3 项目 ???对react相关代码库以及框架的源码进行了一定的分析 ?react16.6 View contents 源码实例分析:可见runl ...

  5. Android之图片加载框架Picasso源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/76645535 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

  6. android网络框架retrofit源码解析二

    注:源码解析文章参考了该博客:http://www.2cto.com/kf/201405/305248.html 前一篇文章讲解了retrofit的annotation,既然定义了,那么就应该有解析的 ...

  7. Android 图片加载框架Gilde源码解析

    1.使用Gilde显示一张图片 Glide.with(this).load("https://cn.bing.com/sa/simg/hpb/xxx.jpg").into(imag ...

  8. Android项目解耦--路由框架ARouter源码解析

    前言 上一篇文章Android项目解耦–路由框架ARouter的使用讲述了ARouter在项目中的使用,这边文章主要对ARouter的源码进行学习和分析. ARouter的结构 ARouter主要由三 ...

  9. 服务注册与发现框架discovery源码解析

    discovery是B站开源的类Eurekad的一款服务注册与发现框架,简单介绍如下: 1. 实现AP类型服务注册发现系统,在可用性极极极极强的情况下,努力保证数据最终一致性 2. 与公司k8s平台深 ...

最新文章

  1. 在Linux系统安装Node.js
  2. window 修改字符集为utf8
  3. 苹果截屏快捷键_新手小白用苹果电脑搞科研,学会这些才不至于尴尬!
  4. PouchContainer 富容器技术解析
  5. JSF范围教程– JSF / CDI会话范围
  6. date format 精辟讲解
  7. 盖茨基金会重发明厕所进展
  8. 订餐系统-第一个用NodeJs实现的项目
  9. python3 zip函数
  10. sqlserver差异备份3117
  11. 我的世界1.8正版服务器大全,史上最全服务器汇总 我的世界1.8服务器地址大全...
  12. SQL Server数据行的物理空间分配
  13. 遗传算法实例解析(python)
  14. Redis常用命令总结
  15. java百鸡问题_算法-百鸡问题 Java
  16. 服装尺寸 html,女装标准尺码对照表,服装尺寸对照表,衣服尺码对照表
  17. 学生HTML个人网页作业作品~蛋糕甜品店铺共11个页面(HTML+CSS+JavaScript)
  18. 软件设计模式“单例模式”和“工厂模式”
  19. 搭建Centos ,配置网络 以及换源
  20. 【带移动搜索功能】织梦dedecms手机WAP插件专业版 织梦自动建手机WAP站 PC+WAP数据同步更新 访问自动跳转

热门文章

  1. FreeRTOS堆栈溢出检查
  2. shell脚本----正则表达式
  3. 搭建Nginx+nginx-rtmp-module的hls流媒体服务器并用OBS进行推流
  4. IT小百科之“手把手教你如何实现秒破加密的Office办公文档”
  5. 基于Java毕业设计中医药科普网站源码+系统+mysql+lw文档+部署软件
  6. Linux强制结束进程
  7. 在 Cacti 下利用 SNMP Informant 实现 Windows 监控
  8. 你想尝试Web3工作吗?看这篇文章就够了
  9. cfa-esg网课资源、教材资源、习题资源
  10. admi后台 vue_Vue管理后台框架选择推荐