学习目标
- Vue.js 的静态成员和实例成员初始化过程
- 静态成员如:Vue.use、Vue.set、Vue.nextTick
- 实例成员如:vm. e l 、 v m . el、vm. el、vm.set、vm.$mount
- 首次渲染的过程
- 数据响应式原理
准备工作
获取源码
- 项目地址:https://github.com/vuejs/vue
- Fork 一份到自己的仓库,克隆到本地,可以自己写注释提交到 github
- 注意提交的时候要按照规范写提交日志,否则提交失败
- 例如:
docs: 乱写一通
- 例如:
- 注意提交的时候要按照规范写提交日志,否则提交失败
- 版本选择:Vue 2.6
- 目前为止 (2020-07) Vue 3.0 还没有发布正式版
- 新版本是兼容2.6的,发布后,现有项目不会升级到 3.0,2.x 还有很长一段过渡期
- 3.0 项目地址:https://github.com/vuejs/vue-next
目录结构
- dist - 打包的结果
- Vue 打包后会生成很多不同的版本,这些版本在 dist/README.md 中都有相应的介绍
- examples - 示例
- 快速体验Vue的基本使用方式
- src - 源码。根据不同的功能,拆分到不同的文件夹
- compiler - 编译器,把模板(template)转换成render函数,render函数创建虚拟DOM
- core - Vue 的核心
- components 定义了 keep-alive 组件
- global-api 定义了Vue的静态方法
- instance 创建Vue实例,定义了Vue的构造函数、初始化、生命周期等相应函数
- observer 实现响应式机制
- util 公共成员
- vdom 虚拟DOM
- 重写了 Snabbdom,增加了组件的机制
- platforms - 和平台相关的代码
- web - web 平台下相关代码
- entry-*.js 打包时候的入口
- weex - 基于Vue的移动端开发框架 weex 平台下的相关代码
- web - web 平台下相关代码
- server - 服务端渲染的相关代码(Vue2.0后支持服务端渲染)
- sfc - 单文件组件 Single Function Component
- sfc中的代码会将单文件组件转化成JS对象
- shared - 公共的代码
了解 Flow
Vue 2.x 是使用 Flow 开发的。
Vue 3.0 已经使用 TypeScript 开发,所以没有必要深入学习Flow。
- Flow 和 TypeScript 都是 静态类型检查器。TypeScript更强大。
- 它们都是基于 JavaScript 的,是JS的超集。
- 最终都会编译成 JavaScript。
- JavaScript 本身是动态类型检查,代码在执行过程中检查代码是否正确。
- 静态类型检查:代码在编译(执行前)的时候检查代码是否正确。
- 一般在大型项目中,使用静态类型检查,来确保代码的可维护性和可读性。
Flow 使用:
- Flow 的 静态类型检查错误 是通过 静态类型推断 实现的。
- 文件开头 通过
// @flow
或者/* @flow */
声明该文件需要 Flow 进行静态类型检查 - 代码写完后,需要结合Flow官方提供的命令行工具,或vscode的插件,来检查类型是否错误。
在方法中的 形参 后通过冒号指明 参数 所使用的类型,在括号后使用冒号指明 方法返回值 的类型:
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error
调试
了解如何对Vue的源码进行打包和调试。
看源码的过程中,可以通过调试,来验证自己的一些想法。
打包
打包工具
Vue 源码中使用的打包工具是 Rollup。
- Rollup 和 Webpack 都是打包工具,Rollup 比 Webpack 更轻量
- Webpack 把所有文件当作模块,Rollup 只处理 js 文件,更适合在 Vue.js 这样的库中使用。
- Rollup 打包不会生成冗余的代码。
- Webpack 会生成一些支持浏览器端的代码。
- 开发库的时候适合使用Rollup,开发项目适合使用 Webpack。
打包过程
- 安装依赖
npm i
- 设置 SourceMap
- package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
- 执行 dev
npm run dev
,打包使用的是Rollup-w
监听文件变化,文件变化自动重载,更新结果-c
指定配置文件--environment
设置环境变量- 通过设置环境变量
TARGET
不同的值,来打包生成不同版本的 Vue
- 通过设置环境变量
注意:
- dev 脚本会生成单个版本的打包文件到dist目录。
- build 脚本会生成所有版本的打包文件到dist目录。
为了便于区分,可以将dist清空,或拷贝后置空,来查看dev打包后的结果。
dev 和 build 都不会清空 dist 目录,也不会生成 README.md 文件(可以提前备份)。
开始调试
选择 examples 中的一个示例,如 grid。
将引入的 vue.min.js 改为 vue.js(dev生成的打包文件)。
浏览器打开示例的 index.html
F12 打开开发人员工具,在source面板中找到grid.js代码中创建vue实例的位置,添加断点,刷新页面。
页面到断点停止,F11 跳转到 src 下具体的文件,接下来就可以调试了。
Vue 的不同构建版本
npm run build
重新打包生成所有版本的Vue。- 官方文档 - 对不同构建版本的解释
源码(dist/README.md)中的解释:
打包文件说明
UMD | CommonJS | ES Module | ||
---|---|---|---|---|
开发版本(未压缩版本) | Full(完整版) | vue.js | vue.common.js | vue.esm.js |
开发版本(未压缩版本) | Runtime-only(运行时版) | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
生产版本(压缩版本) | Full (production) | vue.min.js | ||
生产版本(压缩版本) | Runtime-only (production) | vue.runtime.min.js |
术语
- Full(完整版): 同时包含 编译器 和 运行时 的版本。
- Compiler(编译器): 用于将模板字符串(template)编译成 JavaScript 渲染函数(render函数)的代码,体积大(3000行代码),效率低。
- Runtime(运行时): 用于创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,基本上就是除去编译器的代码。体积小(相对少了3000行代码)、效率高。
- UMD: UMD版本的通用模块化版本。
- UMD规范
- 支持多种模块化方式,CommonJS、CMD、AMD 以及 挂载到window对象上使用
- 可以通过
<script>
标签引入,在任意规范的项目中运行。 - 内部其实是通过检测当前使用环境和模块的定义方式(判断
define
、exports
等),将各种模块化定义方式转化为同样的一种写法。
- 默认文件
vue.js
就是 运行时 + 编译器 的UMD版本
- UMD规范
- CommonJS(cjs): CommonJS 版本用来配合老的打包工具,如 Browserify 或 webpack 1
- ES Module(esm): ES Module 版本用于配合现代打包工具,如 webpack2 或 Rollup。
- ESM 是未来主流模块化方式,重点了解。
- ESM 格式被设计为可以被静态分析(在编译时处理解析,而不是在运行时),所以打包工具可以利用这一点来进行 Tree Sharking,将用不到的代码排除出最终的包。
使用Vue CLI创建的项目,默认使用的 ESM 运行时版本,即 vue.runtime.esm.js。
开发版本 vs 生产版本
- UMD 构建的开发/生产版本是硬编码的。
- 未压缩的用于开发
- 压缩的用于生产
- CommonJS 和 ESM 构建的是用于配合 打包工具的。
- 所以压缩任务交给打包工具,Vue不为它们提供压缩版本。
完整版 vs 运行时
<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> -->
<script src="../../dist/vue.runtime.js"></script>
<script>
// runtime
// 运行时版本,不支持编译 tempalte,需要直接编写 render 函数
const vm = new Vue({
el: '#app',
render(h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})
</script>
运行时版本相比完整版体积小(轻30%),效率高,推荐使用。
Vue CLI 默认版本(运行时)
在Vue CLI创建的项目根目录执行命令vue inspect > output.js
查看webpack的配置(不是有效的webpack配置文件)。
在文件中找到resolve
选项,查看别名alias
配置中,vue模块的地址指向 vue/dist/vue.runtime.esm.js
,即 ESM的运行时版本。
所以import Vue from 'vue'
引入的就是上面这个文件。
SFC 单文件组件
浏览器环境不支持 SFC 类型的文件(.vue),所以打包过程中,打包工具会将 SFC 转换成 JS 对象(如 vue-loader)。
在转换过程中,就会把 tempalte 模板转换成 render 函数,所以 SFC 在运行时不需要编译器。
所以 runtime 版本中可以使用 SFC。
入口文件
寻找入口文件
在开始看源码之前,先找到入口文件。
通过查看 dist/vue.js
的构建过程,找到入口文件。
dev 脚本用于打包生成 dist/vue.js
文件。
-c scripts/config.js
指定配置文件--environment TARGET:web-full-dev
指定环境变量 TARGET- TARGET - 指定构建版本
- web - 浏览器环境
- full - 完整版
- runtime - 运行时版本
- cjs - CommonJS版本
- esm - ES Module 版本
- dev - 开发版本
- TARGET - 指定构建版本
查看 dev 脚本中指定的配置文件 scripts/config.js
。
配置文件一般是一个模块,模块底部一般会导出一些成员,查看底部:
// 判断环境变量是否有 TARGET
// 如果有,使用 getConfig() 生成 rollup 配置文件
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
查看 genConfig 方法:
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry, // rollup入口文件
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)
}
}
}
// ...
return config
}
查看 builds ,它存储了每个环境变量的值对应的一些配置信息,找到web-full-dev
:
const builds = {
// ...
// 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' },
// 文件头:包含vue版本等信息
banner
},
//...
}
项目目录中没有入口文件路径中的web
目录,查看resolve
方法,它将解析传入的路径:
const aliases = require('./alias')
const resolve = p => {
// 根据路径中的前半部分去 alias 中找别名
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
查看 alias 模块:
const path = require('path')
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'),
// web 对应的路径
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
所以入口文件就是:src/platforms/web/entry-runtime-with-compiler.js
是一个 runtime + compiler 的配置。
从入口开始调试
从入口开始调试,查看以下场景:
// 当同时定义了template和render会渲染什么?
const vm = new Vue({
el: '#app',
tempalte: '<h3>Hello tempalte</h3>',
render(h) {
return h('h4', 'Hello render')
}
})
查看入口文件中的$mount
方法:
// 获取 $mount 初始定义
const mount = Vue.prototype.$mount
// 重写 $mount 方法,增加功能
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取 el 对象
el = el && query(el)
// el 不能是 body 或 html
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// 判断是否定义了 render 选项
if (!options.render) {
// 如果没有定义 render 函数,获取tempalte选项,进行处理
let template = options.template
// ...
if (template) {
// ...
options.render = render
// ...
}
}
}
// 调用 mount 方法,渲染DOM
return mount.call(this, el, hydrating)
}
// src\platforms\web\util\index.js
// query 方法
export function query (el: string | Element): Element {
// 判断是字符串还是dom对象
if (typeof el === 'string') {
// 如果是字符串,就认为是选择器
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
// src\platforms\web\runtime\index.js
// $mount 方法初始定义:渲染DOM
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
运行 dev 生成vue.js,开始调试。
在入口文件的$mount方法内第一行打断点,刷新页面,查看Source面板的Call Stack。
Call Stack 记录调用栈,从下到上是依次执行的方法。
依次点击调用栈,可以跳转到调用方法的位置。
可以追踪到 Vue.$mount 的调用来源。
继续调试,由于定义了render,$mount 内部直接调用并返回 mount 方法。
最终页面渲染的是 render 定义的内容。
总结
- 阅读源码记录
- el 不能是 body 或 html 标签
- 如果没有 render ,把template 转换成 render 函数
- 如果有render,直接调用 mount 挂载 DOM
- 调试代码的方法
- 断点
- 调用堆栈 - 查看方法之间调用关系