投身烈火
Keep Walking

vue源码阅读笔记(1)

其实我早就开始读vue的源码了,那会儿还是1.x版本,但是因为懒没有坚持下来,现在都已经更新到2.x了……总之坚持读完吧。vue的源码还是比较好读的,因为注释清楚又有中文文档对照,所以作为读源码练手的对象非常合适。这次我读的2.20的release版本,我阅读的习惯是,不只看大概流程,会把每个函数都看一遍,学习借鉴细节。之后的笔记也会以这个思路来写,希望能给自己和看的人带来帮助吧。

版本

分支:master
commit id:2a19f911dc8631d44b7c7e63c4db57ef28ac5e69
版本:2.20 release

目录结构

简单写下vue的目录结构,标注下他们都是干嘛用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
|
|-- + benchmarks 基准测试,用来测性能的
|-- + build 构建脚本主要都放这里
|-- + dist 构建后的web端版本的输出目录
|-- + examples 各种vue使用的例子
|-- + flow flow的规则文件,flow是facebook出的一套检验js变量类型的框架
|-- + packages 构建后server side render和weex版本的输出目录
|-- + src 构建前的源码
|-- + test 各种测试用例
|-- + types 类型检查测试的部分,用typescript写的
|-- .babelrc 转es5的配置,vue用的不是babel用的bubble
|-- .eslintrc eslint的配置
|-- .eslintignore eslint忽略的文件夹
|-- .flowconfig flowtype的配置文件
|-- BACKERS.md 捐款列表,二百五那栏还没人捐,想排前排的土豪赶紧行动吧
|-- circle.yml CircleCI集成测试平台的配置文件
|-- package.json 所有工作流的命令都定义在scripts里面
|-- yarn.lock yarn生成的依赖文件,估计开发过程中用的yarn替换了npm
*/

虽然这感觉很多余,但是对于小白来说应该很重要吧,我刚看的时候为了搞明白flowbenchmarks是个啥多少浪费了写时间……

打包构建

vue是用npm的scripts来定义工作流命令的,貌似用这种方式取代grunt、gulp已经越来越流行了呢……构建命令大体分为四类,dev、build、test、release,下面列出了所有的命令,并注释了是做啥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
//...
"scripts": {
/*------ develop ------*/
"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
"dev:cjs": "TARGET=web-runtime-cjs rollup -w -c build/config.js",
"dev:ssr": "TARGET=web-server-renderer rollup -w -c build/config.js",
"dev:compiler": "TARGET=web-compiler rollup -w -c build/config.js",
"dev:weex": "TARGET=weex-framework rollup -w -c build/config.js",
"dev:weex:compiler": "TARGET=weex-compiler rollup -w -c build/config.js",
/*------ build ------*/
"build": "node build/build.js",
"build:ssr": "npm run build -- vue.runtime.common.js,vue-server-renderer",
"build:weex": "npm run build -- weex-vue-framework,weex-template-compiler",
/*------ test ------*/
"dev:test": "karma start build/karma.dev.config.js",
"test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr",
"test:unit": "karma start build/karma.unit.config.js",
// 单元测试
"test:cover": "karma start build/karma.cover.config.js",
// 覆盖率测试
"test:e2e": "npm run build -- vue.min.js && node test/e2e/runner.js",
// e2e(end to end,就是所谓的“用户真实场景”)测试
"test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.json",
// weex 的单元测试
"test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.json",
// sever side render 的单元测试
"test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
"test:types": "tsc -p ./types/test/tsconfig.json",
// 类型校验
"lint": "eslint src build test",
// 规范校验
"flow": "flow check",
// 类型校验
"sauce": "SAUCE=true karma start build/karma.sauce.config.js",
// 兼容性测试
"bench:ssr": "npm run build:ssr && NODE_ENV=production node benchmarks/ssr/renderToString.js && NODE_ENV=production VUE_ENV=server node benchmarks/ssr/renderToStream.js",
// 基准测试,用来测性能的
/*------ release ------*/
"release": "bash build/release.sh",
"release:weex": "bash build/release-weex.sh",
"install:hooks": "ln -fs ../../build/git-hooks/pre-commit .git/hooks/pre-commit"
}
//...
}

dev系列命令是开发框架的时候用的。看完所有命令之后知道了以下这些信息:

  • dev系列命令都是 TARGET=XXX rollup -w -c build/config.js 的形式。可以看出vue打包用的是rollup
  • -w 是watch,-c 是指定config文件,build/config.js是rollup的配置文件。build/config.js 内部根据 TARGET 参数获取不同的构建配置。
  • 话说使用自己的项目也是使用rollup打包的话,build/config.js 可以作为很好的参考或者模板呢。
  • 使用的rollup插件:
    • rollup-plugin-flow-no-whitespace
      插件用来去掉flow使用的类型检查代码。有趣的是,插件是还是作者自己写的,只是为了想去掉打包后遗留的空格……还真是洁癖呢,噗噗……
    • rollup-plugin-buble
      替代babel,用来转换es5用的。
    • rollup-plugin-alias
      用来配置打包过程中各个模块的路径映射,具体的配置写在 build/alias.js 中。这样代码中就可以用src作为根目录引用模块了。值得注意的是,src/platforms 目录下的 web 模块和 weex 模块,也都做了映射,所以在看代码时有 import xxx from ‘web/xxx’的引用,就都是从 platforms 下引用的。貌似这是缩短引用路径、区分目录结构和代码逻辑的好方法呢,实际开发中也可以借鉴。
  • 简单查了一些rollup的资料,rollup的特性包括:

    • 打包后的代码没有 require,import的,而是直接插入到文件中
    • 可以生成 AMD,CMD,UMD 甚至 ES6 模块文件
    • tree-shaking,会移除未使用到的 ES6 exports模块,打包后的文件体积更小
    • 配置简单
    • 没有自带的模块机制,使用es6原生的模块依赖机制

    虽然webpack2也支持tree-shaking,但是从rollup的配置简单、功能单一、打包文件没有多余代码这些特点俩看,感觉很适合用来打包独立库或者框架这种都是js并且结构相对简单的项目呢。

  • 相比之前,现在的dev纯粹是按照不同参数做打包并且watch了,我记得1.x版本还带webpack-dev的调试服务器来着,现在的这套比之前的轻了很多,估计打包速度也会快不少,这点和vue本身的理念也有相符吧?

build系列命令用来打包所有配置。总结下看到的知识:

  • build系列命令都是运行 build/build.js 这个文件。这个文件中的逻辑就是通过 build/config.js 获取所有的配置,然后串行用rollup打包。
  • 后面的参数可以用来过滤要打包的配置,获取参数和过滤的逻辑也是写在 build/config.js 里面的。
  • 如果想编写串行执行任务和获取参数做过滤一类的工作流脚本,又不想借助grunt、gulp之类的任务管理库,build/build.js 里部分的这两部分代码可以作为很好的参考。

test系列命令是用来搞自动化测试的,具体的分析:

  • 测试这部分包含的内容很多,现在只是粗略的看了看。先读源码,后续再详细解读这部分的内容
  • 大部分命令是做啥的我都标出来是干嘛的了,其他命令只是对其他命令的一个封装,话说包含的测试还真是全呢……
  • 其实这些命令也都不是让你自己执行的,这些都是用来搞自动化测试的,自动自动化测试的命令配置在 build/ci.sh 这个脚本文件里面。这个脚本会在CircleCI的hook中被调用。话说想搞持续集成的可以参考这个配置呢。
  • 同时使用了facebook的flow和typescript做类型检查,在类型检查这方面真是费了牛劲了……

release系列命令是用来发布rlease版本的:

  • 调用了build文件下对应的sh文件,对于windows用户还真是不友好呢哈哈哈
  • 脚本里主要做了设置版本、自动化测试、构建、打tag、提交、npm推送这几件事
  • 还提别为weex做了独立的发布脚本,看来还真是深度合作呢
  • 其实如果团队都是用mac或者linux,或者都用开发机,可以用这套脚本作为工作流中的一个环节,自动发布提交。那句话怎么说的来着,“重复七次以上的工作都应该自动化”?大概吧……

源码 src

接下来看具体源码,所有的源码都在src目录下,先看下src目录下文件的结构:

1
2
3
4
5
6
7
8
9
10
/*
|
|-- + compiler 解析模板用的?
|-- + core vue的核心,
|-- + entries 各种入口的封装
|-- + platforms 不同平台下自己独特的模块
|-- + server server side render的
|-- + sfc 用来将.vue文件转坏为sfc(可识别组件)对象的
|-- + shared 共享的模块,一个工具集
*/

刚才说到的 build/config.js 文件,里面标记了所有的打包配置,从打包配置中可以看出,所有的入口都在 src/entries 文件夹中,我阅读源码的习惯是从入口开始读。

入口 src/entries

src/entries 的目录结构如下:

1
2
3
4
5
6
7
8
9
10
/*
|
|-- web-compiler.js 只包含vue的模板解析器和.vue解析器
|-- web-runtime.js 只包含vue的运行时部分的代码
|-- web-runtime-with-compiler.js 这个模块既包含解析器又包含运行时
|-- web-server-renderer.js server side render 用的模块,和客户端的不一样,不分解析器和运行时
|-- weex-compiler.js weex的解析器
|-- weex-factory.js weex的运行时
|-- weex-framework.js 这个貌似是weex的框架?因为不了解weex,所以只能靠猜的了
*/

具体每个文件的用途已经在上面简单的标注出来了,主要包含web端,server端,客户端(weex)三部分,下面详细解读各个文件,因为对server端和客户端并不熟悉,所以这部分只能粗浅的猜猜了……

web-compiler.js

这个文件比较简单,就是导出了解析sfc模块和compiler模块的接口。compiler模块的作用是用来解析模板的,对应的是 src/compiler 模块,粗略的看了下是使用new Function将字符串转换为js代码,所以对于不支持或者认为这样不安全的环境,vue会给出错误提示。具体的源码后续再继续详细读。

web-runtime.js

感觉web-runtime是对core的vue模块做了再加工

大概逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//...
// 添加不同平台下的功能函数,web平台下都有对应的的接口,weex平台下都是空函数
// 各函数的具体用途看到了再解释吧
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 添加不同平台下的组件和命令
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// 安装虚拟dom的补丁函数,貌似只有在客户端下才会用到,服务端是没有的,另外weex也有自己的补丁函数,所以这里知识安装浏览器的patch函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 定义$mount函数,只是对核心的mountComponent方法进行了个简单的封装
Vue.prototype.$mount = function (el, hydrating) {
// ...
// core/instance/lifecycle模块下的mountComponent
return mountComponent(this, el, hydrating)
}
// 定义devtool的全局钩子(hook)
// vue有自己的chrome插件调试工具,下面这段代码就是启动调试工具的
setTimeout(() => {
// 判断是否配置了使用调试工具,其实就是看是不是生产版本……
if (config.devtools) {
// 判断是否安装了调试工具,是通过检查全局变量window.__VUE_DEVTOOLS_GLOBAL_HOOK__来判断的
if (devtools) {
// 如果有就触发调试工具的init事件,所以如果项目中使用了生产版本的vue或者没有使用vue,调试工具都不会启动的
devtools.emit('init', Vue)
} else if (process.env.NODE_ENV !== 'production' && isChrome) {
// 如果没有安装调试工具,并且使用的不是生产版本的vue,用的还是chrome浏览器,就提示用户下载调试工具
console[console.info ? 'info' : 'log'](
// ...
)
}
}
// 如果不是生产版本,提示用户现在使用的是开发版本,正式部署的时候用生产版本
// config中的productionTip和devtools其实都是process.env.NODE_ENV,貌似在开发过程中应该可以配置的说
if (process.env.NODE_ENV !== 'production' &&
config.productionTip !== false &&
inBrowser && typeof console !== 'undefined') {
console[console.info ? 'info' : 'log'](
// ...
)
}
}, 0)
// 最后导出Vue模块
export default Vue

web-runtime-with-compiler.js

这个文件作为一个入口,将已经整合好的compiler和runtime再一次整合封装,最终导出浏览器用的vue构造函数。

代码的大概逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// ...
// 定义一个纯函数,之前我在mobx的文章中写过,纯函数的输入、输出都是固定的,所以可以用来做缓存
// cached函数虽然表面上看引用自core/util/index,实际是core/util/index引用了shared/util,cached定义在shared/util中
// 利用纯函数做缓存的技巧可以应用在自己的项目中,虽然定义个对象也能搞定,但是这么封装真的是很美观呀~
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 定义了一个工具函数,看名字就知道是获取元素outerHTML的
// 之所以这么处理貌似是因为IE在取svg元素的outerHTML时有bug
function getOuterHTML (el) {
// 判断元素是否有outerHTML属性
if (el.outerHTML) {
// 有就直接使outerHTML
return el.outerHTML
} else {
// 没有就建个空div把要获取的元素赋值进去,然后取innerHTML
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
// $mount方法是用来挂载实例的
// 这里定义的$mount是对之前在web-runtime.js里定义的$mount进行封装
// 先做个临时变量保存原来的$mount
const mount = Vue.prototype.$mount
// 然后定义新的
Vue.prototype.$mount = function (el, hydrating) {
// ...
// 判断el是否body或者html
if (el === document.body || el === document.documentElement) {
// ...
// 如果是就中断,也就是说vue是无法在dom的根节点上挂载的
return this
}
// $options当前 Vue 实例的初始化选项
const options = this.$options
// 判断$options是否有render函数
if (!options.render) {
// 没有就造个render函数出来
// ...
// 判断$options是否有template属性
if (template) {
// 如果有template属性
// 判断template属性是否是字符串
if (typeof template === 'string') {
// 如果template是字符串,则把他当做selector使用
// 判断selector是否是唯一的(是不是id)
if (template.charAt(0) === '#') {
// 如果selector是唯一的,则使用selector的innerHTML作为模板
// 并且缓存模板内容
template = idToTemplate(template)
// ...
}
// 判断template属性是否是dom节点
} else if (template.nodeType) {
// 如果是dom节点则用innerHTML作为template
template = template.innerHTML
} else {
// ...
// 如果template不符合要求,则中断程序
return this
}
} else if (el) {
// 如果没有template属性
// 则取挂载dom节点的outerHTML作为template
template = getOuterHTML(el)
}
// 如果经过之前的过程获取到了template
// 则根据获取到的template生成render函数
if (template) {
// ...
// 根据template生成render
// 另外在开发版本中,还会利用window.performance统计生成render的时间,分析性能,因为这部分代码不是主要代码,所以我就删掉了
const { render, staticRenderFns } = compileToFunctions(template, {
// 用来标记是否需要转换换行符的,为了兼容IE的,貌似IE和其他浏览器在处理换行符时的操作不一样啊
shouldDecodeNewlines: shouldDecodeNewlines,
// 对应 https://cn.vuejs.org/v2/api/#delimiters ,纯文本插入分隔符,可在构建时修改
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
// ...
}
}
// 调用 web-runtime.js 中定义的$mount
return mount.call(this, el, hydrating)
}
// 将解析compile模块也绑定到Vue上
Vue.compile = compileToFunctions
// 导出Vue类
export default Vue

web-server-renderer.js

这个是server side render的入口,所以与brower端用到的方法差别很大。server端只是做初步的渲染,所以只有一个生成render的函数,结构比较简单。

1
2
3
4
5
6
7
8
9
// ...
// 定义一个生成render对象的函数
export function createRenderer (options) {
return _createRenderer({
// ...
})
}
// 定义一个生成render函数的工厂
export const createBundleRenderer = createBundleRendererCreator(createRenderer)

这部分的功能划分的很细,做了好多的高阶函数,粗略的看了下看的段点儿晕,后续看到不同平台的代码时再详细看。

weex-compiler.js

对应web-compiler.js,导出对应平台下的compiler模块

weex-factory.js

对应web-runtime.js,只不过这里没有添加独特的函数,直接导出的对应平台下的runtime模块

weex-framework.js

导出了weex/framework这个模块下的所有方法,貌似是给weex提供基础支持用的?具体的还没开始仔细看。

总结

总的来说这次记录了整个项目的入口部分的代码。通过这些入口,可以了解所有主要的模块的用途,项目的结构等等基础信息。当然,也能学习借鉴不少知识,我觉得以下这些点值得记录并应用到自己的项目中:

  • npm script的定义规则和分类
  • flow和typescript做类型检查的方法
  • 打包测试发布整套的工作流定义
  • 通过封装重写的方式不断扩展接口
  • 通过纯函数的特性做缓存
  • 通过高阶函数拆分模块(具体模块的划分思想我还没看出来……)

那么你又从这部分代码中悟到了什么呢?有想法就留言告诉我吧,咱们一起交流交流~( ̄▽ ̄)

好的那么由于时间不足本次的博客就到这里,话说这次时间实在是太仓促了,而且解读源码的表达方法我也还没有探索好……我的感觉就是,虽然我都看懂了,但是却说不明白,下次会尝试着配上些流程图或者思维导图来记录,这样也更容易理解吧。

那么如果不出意外的话,大概可能maybe也许下周五会更新吧~!这次就不别安利了,毕竟我自己都觉得好坑啊……就这样了……

白了个白~!

update 2017-03-30

折腾了两个礼拜,终于想到一个自己比较满意的方式来做记录,把之前的文章大修了一遍,自我感觉还是不错,希望以后自己再看的时候不会觉得尴尬吧,哈哈哈……

另外我还发现了一个gitbook,也在读vue的源码,不过他在几个月之前断更了……但是这种形式真心不错,后续我也打算用这种形式再整理一遍的说~