「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
一、背景
为啥还不开始源码?是不是在故意灌水?
冤枉,真冤枉😂😂😂...
本系列小作文,立意就是让看这个文章的每个人都能看懂。本来第三篇是写 new Vue()
过程的,但是在这个过程中,我发现不能很好的说明白 vue
的入口文件是 core/incetance/index.js
,以至于我无法说明白 initGlobalAPI(Vue)
先于 new Vue()
执行,故而加了一这一篇相当于 Vue 的源码编译相关的文章。
另外,不知道是不是我这样,当我们看代码的时候一般从入口开始看,入口中又各种 import/export
操作过后,其实忽略了一个重要事实,被 import
的模块其实要先于当前模块的加载和执行的。看代码是”先序遍历“,而事实上代码的加载执行则是个“后序遍历”。
这就会有一个问题,就是看代码的时候这个方法作用是啥不清楚,用到的变量代表的值是啥不知道,而事实上是代码倒着来的,当这行到这里的时候,用到的方法和变量都已经在子模块中加载并执行了。
所以就有了这篇文章分析这个过程,先培养一点感觉,另外说明这个入口的问题。
二、Vue 的打包命令
Vue
的源代码打包命令有很多,这里仅仅以我们使用的包含编译器的完整版打包命令 dev
进行分析,在 Vue
源码目录下的 package.json
的 scripts
中:
{
"name": "vue",
"version": "2.6.14",
"scripts": {
"dev": "rollup --sourcemap -w -c scripts/config.js --environment TARGET:web-full-dev",
}
}
2.1 参数 -c
-c
是为 rollup
指定配置文件,所以配置文件的路径就是 scripts/config.js
2.2 参数 --environment TARGET:web-full-dev
指定环境变量参数,这个 --environment TARGET:web-full-dev
是向 process.env
上增加 TARGET
属性,值是 web-full-dev
,记住这个 TARGET
的值,后面再读取打包配置时很有用;
三、打包配置的读取
3.1 vue/scripts/config.js 文件
rollup
打包也需要一个入口,就像 webpack
的 entry
一样的一个东东。在 Vue
的编译时设计中,为适应不同的场景的版本,例如有没有编译器、是 ESModule
还是 CommonJS
等,vue
设计了不同的入口,通过这些不同的入口,最终的输出产物也是不同的。这些逻辑都封装在 vue/scripts/config.js
中,代码如下:
// ....
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
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' },
banner
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
// ....
}
}
注意上面 const builds = { 'web-full-dev': { ... } }
对象,这个对象表示的是一个全集,所有的打包入口相关配置都在这个对象里面,上面的代码省略了很多版本的入口。
其中 'web-full-dev'
是不是很熟悉,没错,就是前面 --environment TARGET:web-full-dev
这个环境参数,那么又是如何取到这个参数 web-full-dev
的呢?
3.2 genConfig 方法结合环境变量
scripts/config.js
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
// ... other properties
return config
}
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
// ... other properties
return config
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
}
接着看上面 scripts/config.js
后面有个方法 genConfig
,这个方法接收一个形参 name
,接收的是环境变量 process.env.TARGET
参数。
后面会判断 process.env.TARGET
参数是否存在,即 if (process.env.TARGET)
,条件成立则调用 genConfig(process.env.TARGET)
,而 genConfig
中,从 builds
对象中以 process.env.TARGET
为 key
获取具体配置,然后加工,最后返回一个包含配置信息的 config
对象。
以我们前面 process.env.TARGET
为 web-full-dev
例子,builds[name]
就取到如下配置:
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), // rollup 打包的的入口
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
四、web-full-dev 的入口
经过前面的分析,我们已经得知 rollup
配置的入口为:entry: resolve('web/entry-runtime-with-compiler.js')
,resolve
这个方法用于解析路径,所以最终的 web-full-dev
入口文件路径为:src/platforms/web/entry-runtime-with-compiler.js
。
我们现在已经找打到了入口文件,接着我们分析一下,从入口入口如何到 Vue 构造函数的,这些模块的依赖关系是怎样的呢?
Vue
构造函数的导入路径:entry-runtime-with-compiler.js
,这个是入口文件模块,它从 runtime/index.js
模块下导入 Vue
,而 Vue
并不是在这个模块声明的变量,而先经 core/index.js
从 src/core/instance/index.js
中导入的,上代码:
4.1 src/platforms/web/entry-runtime-with-compiler.js
import config from 'core/config'
import Vue from './runtime/index' // 从 runtime/index 导入 Vue
// 重写 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
return mount.call(this, el, hydrating)
}
function getOuterHTML (el: Element): string {}
Vue.compile = compileToFunctions
export default Vue
4.2 src/platforms/web/runtime/index.js
这个模块从 core/index
导入 Vue
:
import Vue from 'core/index' // 从 core/index 中导入 Vue
// .... ignore some unnecessary code
export default Vue
4.3 src/core/index.js
这个模块从 ./incetance/index
导入 Vue
,然后执行我们熟悉的 initGlobalAPI
方法并传入 Vue
构造函数:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
// some unnecessary code is ignored ....
initGlobalAPI(Vue)
Vue.version = '__VERSION__'
export default Vue
4.4 src/core/instance/index.js
这个模块则声明了 Vue
的构造函数,并通过 initMixin
方法为,Vue
的原型上扩展了 _init
方法,而 _init
是主入口,在下一篇的源码中会体现,其实当你 new Vue()
的时候,Vue
构造函数就执行了这个 _init
方法。
import { initMixin } from './init'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
// other mixin calling are ignored
export default Vue
五、总结
结合开头背景所讲:人看代码时”先序遍历“,浏览器执行代码时”后序遍历“:
所以入口为 src/platforms/web/entry-runtime-with-compiler.js
时,他依赖的 ./runtime/index
已经加载并执行了,此时已经改写了 Vue.prototype.$mount
方法,以此类推当 ./runtime/index
执行时,它依赖的 src/core/index
也已经完成加载执行,它执行了 initGlobalVue()
和 扩展 Vue.prototype._int
方法的 initMixin
,而 src/core/index
执行时 src/core/instance/index
也已经完成加载执行,它完成了 Vue
构造函数的声明和导出。
这一篇多少有点抽象,笔者想说明白一件事,读源码的时候要有一种思路,当前模块导入的内容晚于被导入模块的执行。这个事情对于看源码是一大障碍,所以这里先行讲解。