Vue.js 是用于构建交互式的 Web 界面的库。它提供了 MVVM 数据绑定和一个可组合的组件系统,具有简单、灵活的 API。从技术上讲, Vue.js 集中在 MVVM 模式上的视图模型层,并通过双向数据绑定连接视图和模型。实际的 DOM 操作和输出格式被抽象出来成指令和过滤器。相比其它的 MVVM 框架,Vue.js 更容易上手。
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些。那么,就让我来吧:) 程序结构梳理
Vue.js 是一个非常典型的 MVVM 的程序结构,整个程序从最上层大概分为
这里面大部分内容可以直接跟 Vue.js 的官方 API 参考文档对应起来,但文档里面没有且值得一提的是构造函数的设计,下面是我摘出的构造函数最核心的工作内容。
整个实例初始化的过程中,重中之重就是把数据 (Model) 和视图 (View) 建立起关联关系。Vue.js 和诸多 MVVM 的思路是类似的,主要做了三件事:
所以整个 vm 的核心,就是如何实现 observer, directive (parser), watcher 这三样东西 文件结构梳理Vue.js 源代码都存放在项目的src目录中,我们主要关注一下这个目录 (事实上test/unit/specs目录也值得一看,它是对应着每个源文件的测试用例)。 src目录下有多个并列的文件夹,每个文件夹都是一部分独立而完整的程序设计。不过在我看来,这些目录之前也是有更立体的关系的:
篇幅有限,如果大家有意“通读” Vue.js 的话,个人建议顺着上面的整体介绍来阅读赏析。 接下来是一些自己觉得值得一提的代码细节 一些不容错过的代码/程序细节this._eventsCount是什么?一开始看instance/init.js的时候,我立刻注意到一个细节,就是this._eventsCount = {}这句,后面还有注释
非常好奇,然后带着疑问继续看了下去,直到看到api/events.js中$broadcast方法的实现,才知道这是为了避免不必要的深度遍历:在有广播事件到来时,如果当前 vm 的_eventsCount为0,则不必向其子 vm 继续传播该事件。而且这个文件稍后也有_eventsCount计数的实现方式。
这是一种很巧妙同时也可以在很多地方运用的性能优化方法。 数据更新的 diff 机制前阵子有很多关于视图更新效率的讨论,我猜主要是因为 virtual dom 这个概念的提出而导致的吧。这次我详细看了一下 Vue.js 的相关实现原理。 实际上,视图更新效率的焦点问题主要在于大列表的更新和深层数据更新这两方面,而被热烈讨论的主要是前者 (后者是因为需求小还是没争议我就不得而知了)。所以这里着重介绍一下directives/repeat.js里对于列表更新的相关代码。
首先diff(data, oldVms)这个函数的注释对整个比对更新机制做了个简要的阐述,大概意思是先比较新旧两个列表的 vm 的数据的状态,然后差量更新 DOM。
第一步:便利新列表里的每一项,如果该项的 vm 之前就存在,则打一个_reused的标 (这个字段我一开始看init.js的时候也是困惑的…… 看到这里才明白意思),如果不存在对应的 vm,则创建一个新的。
第二步:便利旧列表里的每一项,如果_reused的标没有被打上,则说明新列表里已经没有它了,就地销毁该 vm。
第三步:整理新的 vm 在视图里的顺序,同时还原之前打上的_reused标。就此列表更新完成。 顺带提一句 Vue.js 的元素过渡动画处理 (v-transition) 也设计得非常巧妙,感兴趣的自己看吧,就不展开介绍了 组件的[keep-alive]特性
Vue.js 为其组件设计了一个[keep-alive]的特性,如果这个特性存在,那么在组件被重复创建的时候,会通过缓存机制快速创建组件,以提升视图更新的性能。代码在directives/component.js。 数据监听机制如何监听某一个对象属性的变化呢?我们很容易想到Object.defineProperty这个 API,为此属性设计一个特殊的 getter/setter,然后在 setter 里触发一个函数,就可以达到监听的效果。
不过数组可能会有点麻烦,Vue.js 采取的是对几乎每一个可能改变数据的方法进行 prototype 更改:
但这个策略主要面临两个问题:
为此 Vue.js 在文档中明确提示不建议直接角标修改数据
同时 Vue.js 提供了两个额外的“糖方法”$set和$remove来弥补这方面限制带来的不便。整体上看这是个取舍有度的设计。我个人之前在设计数据绑定库的时候也采取了类似的设计 (一个半途而废的内部项目就不具体献丑了),所以比较认同也有共鸣。 path 解析器的状态机设计首先要说parsers文件夹里有各种“财宝”等着大家挖掘!认真看一看一定不会后悔的 parsers/path.js主要的职责是可以把一个 JSON 数据里的某一个“路径”下的数据取出来,比如: var path = 'a.b[1].v' var obj = { a: { b: [ {v: 1}, {v: 2}, {v: 3} ] } } parse(obj, path) // 2 所以对path字符串的解析成为了它的关键。Vue.js 是通过状态机管理来实现对路径的解析的:
咋一看很头大,不过如果再稍微梳理一下:
也许看得更清楚一点了,当然也能发现其中有一点小问题,就是源代码中inIdent这个状态是具有二义性的,它对应到了图中的三个地方,即in ident和两个in (quoted) ident。 实际上,我在看代码的过程中顺手提交了这个 bug,作者眼明手快,当天就进行了修复,现在最新的代码里已经不是这个样子了:
而且状态机标识由字符串换成了数字常量,解析更准确的同时执行效率也会更高。 一点自己的思考首先是视图的解析过程,Vue.js 的策略是把 element 或 template string 先统一转换成 document fragment,然后再分解和解析其中的子组件和 directives。我觉得这里有一定的性能优化空间,毕竟 DOM 操作相比之余纯 JavaScript 运算还是会慢一些。 然后是基于移动端的思考,Vue.js 虽确实已经非常非常小巧了 (min+gzip 之后约 22 kb),但它是否可以更小,继续抽象出常用的核心功能,同时更快速,也是个值得思考的问题。 第三我非常喜欢通过 Vue.js 进行模块化开发的模式,Vue 是否也可以借助类似 web components + virtual dom 的形态把这样的开发模式带到更多的领域,也是件很有意义的事情。 总结Vue.js 里的代码细节还不仅于此,比如:
自己也在阅读代码,了解 Vue.js 的同时学到了很多东西,同时我觉得代码实现只是 Vue.js 优秀的要素之一,整体的程序设计、API 设计、细节的取舍、项目的工程考量都非常棒! 总之,分享一些自己的收获和代码的细节,希望可以帮助大家开阔思路,提供灵感。 |
|