浅曦Vue源码-3-vue.js 的源码入口

561 阅读4分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

一、背景

为啥还不开始源码?是不是在故意灌水?

冤枉,真冤枉😂😂😂...

本系列小作文,立意就是让看这个文章的每个人都能看懂。本来第三篇是写 new Vue() 过程的,但是在这个过程中,我发现不能很好的说明白 vue 的入口文件是 core/incetance/index.js,以至于我无法说明白 initGlobalAPI(Vue) 先于 new Vue() 执行,故而加了一这一篇相当于 Vue 的源码编译相关的文章。

另外,不知道是不是我这样,当我们看代码的时候一般从入口开始看,入口中又各种 import/export 操作过后,其实忽略了一个重要事实,被 import 的模块其实要先于当前模块的加载和执行的。看代码是”先序遍历“,而事实上代码的加载执行则是个“后序遍历”。

这就会有一个问题,就是看代码的时候这个方法作用是啥不清楚,用到的变量代表的值是啥不知道,而事实上是代码倒着来的,当这行到这里的时候,用到的方法和变量都已经在子模块中加载并执行了。

所以就有了这篇文章分析这个过程,先培养一点感觉,另外说明这个入口的问题。

二、Vue 的打包命令

Vue 的源代码打包命令有很多,这里仅仅以我们使用的包含编译器的完整版打包命令 dev 进行分析,在 Vue 源码目录下的 package.jsonscripts 中:

{
  "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 打包也需要一个入口,就像 webpackentry 一样的一个东东。在 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.TARGETkey 获取具体配置,然后加工,最后返回一个包含配置信息的 config 对象。

以我们前面 process.env.TARGETweb-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.jssrc/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 构造函数的声明和导出。

这一篇多少有点抽象,笔者想说明白一件事,读源码的时候要有一种思路,当前模块导入的内容晚于被导入模块的执行。这个事情对于看源码是一大障碍,所以这里先行讲解。