最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,以为真心不错,我的以为 Vue.js 的代码很是之优雅并且精辟,做者自己可能无 (bu) 意 (xie) 说起这些。那么,就让我来吧:)vue
Vue.js 是一个很是典型的 MVVM 的程序结构,整个程序从最上层大概分为node
这里面大部份内容能够直接跟 Vue.js 的官方 API 参考文档对应起来,但文档里面没有且值得一提的是构造函数的设计,下面是我摘出的构造函数最核心的工做内容。git
整个实例初始化的过程当中,重中之重就是把数据 (Model) 和视图 (View) 创建起关联关系。Vue.js 和诸多 MVVM 的思路是相似的,主要作了三件事:github
把 template 解析成一段 document fragment,而后解析其中的 directive,获得每个 directive 所依赖的数据项及其更新方法。好比 v-text="message"
被解析以后 (这里仅做示意,实际程序逻辑会更严谨而复杂):web
this.$data.message
,以及node.textContent = this.$data.message
因此整个 vm 的核心,就是如何实现 observer, directive (parser), watcher 这三样东西api
Vue.js 源代码都存放在项目的 src
目录中,咱们主要关注一下这个目录 (事实上 test/unit/specs
目录也值得一看,它是对应着每一个源文件的测试用例)。数组
src
目录下有多个并列的文件夹,每一个文件夹都是一部分独立而完整的程序设计。不过在我看来,这些目录以前也是有更立体的关系的:缓存
api/*
目录,这几乎是最“上层”的接口封装,实际的实现都埋在了其它文件夹里而后是 instance/init.js
,若是你们但愿自顶向下了解全部 Vue.js 的工做原理的话,建议从这个文件开始看起性能优化
instance/scope.js
:数据初始化,相关的子程序 (目录) 有 observer/*
、watcher.js
、batcher.js
,而 observer/dep.js
又是数据观察和视图依赖相关联的关键instance/compile.js
:视图初始化,相关的子程序 (目录) 有 compiler/*
、directive.js
、parsers/*
directives/*
、element-directives/*
、filters/*
、transition/*
util/*
目录,工具方法集合,其实还有一个相似的 cache.js
config.js
默认配置项篇幅有限,若是你们有意“通读” Vue.js 的话,我的建议顺着上面的总体介绍来阅读赏析。dom
接下来是一些本身以为值得一提的代码细节
this._eventsCount
是什么?一开始看 instance/init.js
的时候,我马上注意到一个细节,就是 this._eventsCount = {}
这句,后面还有注释
for $broadcast optimization
很是好奇,而后带着疑问继续看了下去,直到看到 api/events.js
中 $broadcast
方法的实现,才知道这是为了不没必要要的深度遍历:在有广播事件到来时,若是当前 vm 的 _eventsCount
为 0
,则没必要向其子 vm 继续传播该事件。并且这个文件稍后也有 _eventsCount
计数的实现方式。
这是一种很巧妙同时也能够在不少地方运用的性能优化方法。
前阵子有不少关于视图更新效率的讨论,我猜主要是由于 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 更改:
但这个策略主要面临两个问题:
length
,致使 arr.length
这样的数据改变没法被监听arr[2] = 1
这样的赋值操做,也没法被监听为此 Vue.js 在文档中明确提示不建议直接角标修改数据
同时 Vue.js 提供了两个额外的“糖方法” $set
和 $remove
来弥补这方面限制带来的不便。总体上看这是个取舍有度的设计。我我的以前在设计数据绑定库的时候也采起了相似的设计 (一个半途而废的内部项目就不具体献丑了),因此比较认同也有共鸣。
首先要说 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 里的代码细节还不只于此,好比:
cache.js
里的缓存机制设计和场景运用 (如在 parsers/path.js
中)parsers/template.js
里的 cloneNode
方法重写和对 HTML 自动补全机制的兼容本身也在阅读代码,了解 Vue.js 的同时学到了不少东西,同时我以为代码实现只是 Vue.js 优秀的要素之一,总体的程序设计、API 设计、细节的取舍、项目的工程考量都很是棒!