Appearance
一、分析Vue源码准备工作
指南
阅读源码的过程中,只研究当前目标的整体执行过程,其他的旁支细节先略过,需要的时候再进行分析。
Vue 源码的获取
- 项目地址:https://github.com/vuejs/vue
- Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到 github
- 为什么分析 Vue 2.6
- 到目前为止 Vue 3.0 的正式版还没有发布
- 新版本发布后,现有项目不会升级到 3.0,2.x 还有很长的一段过渡期
- 3.0 项目地址:https://github.com/vuejs/vue-next
源码目录结构
Vue在开发的时候,首先会按照功能把代码拆分到不同的文件夹,再拆分成小的模块,这样代码的结构会很清楚,能够提高代码的可读性和可维护性。我们在项目开发过程中也可以学习这点。
js
dist 打包后的版本
examples 示例,可以快速体验基本使用
src 源码部分
├─compiler 编译相关(模板转换成render函数)
├─core Vue核心库(与平台无关)
├─components Vue自带的keep alive组件
├─global-api Vue的静态方法
├─instance 创建Vue实例(Vue构造函数,生命周期、初始化)
├─observer 响应式机制 ★
├─util 公共成员
└─vdom 虚拟DOM
├─platforms 平台相关代码
├─web web平台下相关代码
├─ entry-*.js 是打包时候的入口(之后会详细看)
└─weex weex平台下相关代码(weex是基于Vue的移动端开发框架)
├─server SSR,服务端渲染(Vue2.0支持服务端渲染)
├─sfc 单文件组件,sfc的代码会将.vue 文件编译为 js 对象
└─shared 公共的代码
了解flow
Vue3.0中已经使用TypeScript开发,flow和TypeScript都是静态类型检查器。TypeScript功能更加强大,他们都是js的超集,都是基于JavaScript的,最终都会编译成JavaScript。JavaScript本身是动态类型检查,代码在执行的过程中检查类型是否正确,c#和Java,都是静态类型检查。静态类型检查是在代码编译的时候检查类型是否正确。使用flow可以有像c#和Java一样的开发体验。
一般大型项目中我们需要静态类型检查来确保代码的可维护性和可读性。所以Vue2.0中引入了flow,flow可以让代码在最小改动的情况下使用静态类型检查。
接下来介绍一下flow的使用。
flow是通过静态类型推断实现类型检查的。
打包 & 调试
Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量。Webpack 把所有文件(js、css、图片等文件)当做模块处理,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用,React源码使用的打包工具也是Rollup。Rollup 打包不会生成冗余的代码。
- 在下载的源码包中安装依赖
npm i
(请使用npm镜像源) - 在
package.json
文件中设置sourcemap
,dev 脚本中添加参数 --sourcemap`
bash
rollup -w -c scripts/config.js --environment TARGET:web-full-dev
# -w watch 监视源码的变化,当源码发生变化的时候,立即重新打包。
# -c 设置配置文件,后面是配置文件的路径
# --environment 设置环境变量,通过后面配置的值生成不同版本的vue
# --sourcemap 开启代码地图,会记录源码和打包之后文件之间的代码对应关系,方便调试,如果代码出错也会精确指出源码中的哪个位置出的错误
# TARGET 打包的vue的版本,web是web平台下,full是完整版,包含编译器和运行时,runtime是只包含运行时,cjs是commonJS模块。dev开发版...
- 执行
npm run dev
进行打包,可以看到dist目录下有了vue.js
和vue.js.map
两个文件。如果要生成不同版本的vue,可以运行npm run build
文件。 - 在example/grid表格案例的index.html中把引入的vue.min.js改为vue.js,打开浏览器调试,可以看到Souces里面有src,这个是引入了sourcemap才会出现的。
在grid.js中设置断点,进行调试。可以看到主构造函数在core/instance/index.js里面,如果不开sourcemap,那么会调试dist/vue.js里面的代码,代码有一万多行,不方便调试。
Vue的不同构建版本
打包之后产生的不同版本的vue
- 使用
npm run build
可以把所有版本的vue都打包出来
版本
- 完整版:同时包含编译器和运行时的版本。
- 编译器(重点了解):用来将模板字符串编译成为 JavaScript渲染函数的代码,体积大、效率低。(创建vue的时候可以传入一个template选项,template选项指明了我们的模板,编译器的作用就是把template转换成渲染函数(render函数,生成虚拟DOM))
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。(不包含编译器,光编译器的代码就3000+,运行时不包含编译器所以体积更小)
不同模块化方式
- UMD:UMD 版本通用的模块版本,支持多种模块方式。 vue.js 默认文件就是运行时 + 编译器的 UMD 版本(包含CommonJs,AMD,也支持把vue实例挂载到全局对象window上)
- CommonJS(简称cjs):CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。(在node中经常使用)
- ES Module(重点了解):从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的 版本。
- ESM 格式被设计为可以被静态分析(编译的时候处理而不是运行的时候,在编译的时候就处理好依赖),所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
- ES6 模块与 CommonJS 模块的差异(阮一峰)
重点了解编译器 + ES Modules,使用vue-cli创建的项目,都是使用的vue.runtime.esm.js版本(运行时版本+ES Modules方式)
在vue-cli中使用
vue inspect
可以看到webpack配置vue inspect > output.js
可以将结果输出到output.js
中 在resolve/alias/vue$中可以看到其选项是vue/dist/vue.runtime.esm.js
, vue$是别名,$是webpack的语法,精确匹配的意思,正常使用用vue即可。App.vue这些单文件组件浏览器不支持,打包的时候会转换成js对象,转换成js对象的时候会帮我们把模板转换成render函数,所以也是不需要编译器组件的。
- 在example里面创建一个项目01,创建index.html,里面引用vue.js版本
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js 01 component example</title>
</head>
<body>
<!-- demo root element -->
<div id="app">
Hello world
</div>
<script src="../../dist/vue.js"></script>
<script>
// compiler
// 需要编译器,把 template 转换成 render 函数
const vm = new Vue({
el: '#app',
template: '<h1>{{ msg }}</h1>',
data: {
msg: 'Hello Vue'
}
})
</script>
</body>
</html>
- 打开浏览器可以看到里面已经用template替换了Hello world
- 再换成vue.runtime.js看到了报错信息
html
<script src="../../dist/vue.runtime.js"></script>
这个报错信息是,运行时版本解析不了template,模板需要完整版才能解析,所以需要我们要么用完整版,要么自己使用render函数
- 使用render函数,再次渲染页面看到显示正常。
html
<script>
// compiler
// 需要编译器,把 template 转换成 render 函数
const vm = new Vue({
el: '#app',
// template: '<h1>{{ msg }}</h1>',
render(h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})
</script>
入口文件
从打包命令
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
可以看到script/config.js
是配置文件,本质其导出了一个配置模块
js
// 判断环境变量是否有 TARGET,其中 process.env.TARGET 是获取环境变量的
// 如果有就使用 genConfig() 生成 rollup 配置文件,并传入环境变量
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
其中用到了genConfig方法。
js
/**
* 生成配置文件
* @param {string} name 环境变量
* @return config对象
*/
function genConfig (name) {
// builds的键是环境名称,值是对应的入口出口环境等配置信息
const opts = builds[name]
// 所有的配置信息
const config = {
input: opts.entry,
external: opts.external,
plugins: [
flow(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
...
// 返回config对象
return config
}
下面详细看一下builds对象的构成,可以看到TARGET后面跟着的参数,是这里的键,里面的值对应了entry、dest等配置文件,设置入口出口格式以及环境等信息。
js
// 环境变量和配置文件的对应关系的对象
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
...
},
...
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
// runtime-only production build (Browser)
'web-runtime-prod': {
...
},
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
...
}
我们可以看到其entry对应的入口是一个resolve()函数,因为传入的参数是web开头,根目录下面并没有web文件夹,所以这里一定是对路径进行了一次转化。
这里把转化的相关函数,以web-full-dev
的入口文件作为参考resolve('web/entry-runtime-with-compiler.js')
js
// config.js
const aliases = require('./alias')
// 将路径转换为绝对路径
const resolve = p => {
// 以/作为分隔第一个元素放入base里,这里base放的是web
const base = p.split('/')[0]
// 上面引入了aliases模块,到alias.js里面找有没有web为键的值
if (aliases[base]) {
// 如果路径存在,通过找可以看到这里的值是根目录下的src/platforms/web的绝对路径,加上web后面的所有路径,所以真正的入口文件路径是src/platforms/web/entry-runtime-with-compiler.js的绝对路径
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
// 如果路径不存在就拼接当前路径
return path.resolve(__dirname, '../', p)
}
}
// alias.js
const path = require('path')
// 转换成绝对路径,path是node方法,__dirname是当前路径(scripts文件夹)的绝对路径,../是scripts的上级目录,再拼接传入的参数p
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
// 在alias.js里面找到了web为键的对象,这里通过resolve确定了值是根目录下的src/platforms/web
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
通过上面的函数可以看到,npm run dev
的入口文件是src/platforms/web/entry-runtime-with-compiler.js
的绝对路径。
从入口文件开始分析 _ platform/web/entry-runtime-with-compiler.js
src/platform/web/entry-runtime-with-compiler.js
看源码遇到的两个问题
语法检查错误
VSCode -> 文件 -> 首选项 -> 设置 -> "javascript.validate.enable": false
改为true
这个时候看到很多地方报错
这个类型只能在.ts文件中使用,VSCode和TypeScript都是微软开发的,所以默认是支持ts语法的,但是VSCode不支持flow,所以这里认为ts的语法只能在.ts中使用,所以默认关闭语法检查,就不会报错了.
高亮显示问题
core/global-api/index.js 下面有一行代码,T表示泛型,但是下面的代码高亮丢失,因为VSCode对这里解析有问题
解决这个问题需要安装插件
安装完成之后可以看到这里高亮显示正常
但是下面有些功能丢失了,Vue.options无法ctrl+左键跳转到定义的地方,这个问题无法解决.