docs: 乱写一通
Vue 2.x 是使用 Flow 开发的。
Vue 3.0 已经使用 TypeScript 开发,所以没有必要深入学习Flow。
Flow 使用:
// @flow
或者 /* @flow */
声明该文件需要 Flow 进行静态类型检查在方法中的 形参 后通过冒号指明 参数 所使用的类型,在括号后使用冒号指明 方法返回值 的类型:
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error
了解如何对Vue的源码进行打包和调试。
看源码的过程中,可以通过调试,来验证自己的一些想法。
Vue 源码中使用的打包工具是 Rollup。
npm i
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
npm run dev
,打包使用的是Rollup
-w
监听文件变化,文件变化自动重载,更新结果-c
指定配置文件--environment
设置环境变量
TARGET
不同的值,来打包生成不同版本的 Vue注意:
为了便于区分,可以将dist清空,或拷贝后置空,来查看dev打包后的结果。
dev 和 build 都不会清空 dist 目录,也不会生成 README.md 文件(可以提前备份)。
选择 examples 中的一个示例,如 grid。
将引入的 vue.min.js 改为 vue.js(dev生成的打包文件)。
浏览器打开示例的 index.html
F12 打开开发人员工具,在source面板中找到grid.js代码中创建vue实例的位置,添加断点,刷新页面。
页面到断点停止,F11 跳转到 src 下具体的文件,接下来就可以调试了。
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 |
标签引入,在任意规范的项目中运行。define
、exports
等),将各种模块化定义方式转化为同样的一种写法。vue.js
就是 运行时 + 编译器 的UMD版本使用Vue CLI创建的项目,默认使用的 ESM 运行时版本,即 vue.runtime.esm.js。
<div id="app">
Hello World
div>
<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 inspect > output.js
查看webpack的配置(不是有效的webpack配置文件)。
在文件中找到resolve
选项,查看别名alias
配置中,vue模块的地址指向 vue/dist/vue.runtime.esm.js
,即 ESM的运行时版本。
所以import Vue from 'vue'
引入的就是上面这个文件。
浏览器环境不支持 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
查看 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: 'Hello tempalte
',
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 or - 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 定义的内容。