目录结构
├── .circleci ----------------------------- 存放持续集成工具circleci的配置文件
├── .github ------------------------------- 存放README.md中关联的md文档
├── benchmarks ---------------------------- 性能测试、评估,跑分demo,比如大数据量的table或者渲染大量SVG
├── dist ---------------------------------- 各个平台构建后文件的输出目录(UMD、CommonJS、ES 生产和开发包)
├── examples ------------------------------ 例子代码
├── flow ---------------------------------- 静态类型声明 [Flow](https://flow.org/)
├── packages ------------------------------ 包含服务端渲染和模板编译器两种不同的npm包,是提供给不同使用场景使用的
├── scripts ------------------------------- 构建相关的文件
│ ├── git-hooks ------------------------- 存放git钩子的目录
│ ├── alias.js -------------------------- 别名配置
│ ├── build.js -------------------------- 对 config.js 中所有的rollup配置进行构建
│ ├── config.js ------------------------- 生成rollup配置的文件
│ ├── feature-flags.js ------------------ 部分属性的标志
│ ├── release.sh ------------------------ 用于自动发布新版本的脚本
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│ │ ├── codegen ----------------------- 把AST转换为Render函数
│ │ ├── directives -------------------- 通用生成Render函数之前需要处理的指令
│ │ ├── parser ------------------------ 解析模版成AST
│ ├── core ------------------------------ 存放通用的,与平台无关的代码
│ │ ├── components -------------------- 包含抽象出来的通用组件,主要是Keep-Alive
│ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法或属性的代码
│ │ ├── instance ---------------------- 实例化相关内容,生命周期、事件等
│ │ ├── observer ---------------------- 响应系统,包含数据观测的核心代码(双向数据绑定)
│ │ ├── util -------------------------- 工具方法
│ │ ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│ ├── platforms ------------------------- 包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
│ │ ├── web --------------------------- web平台
│ │ │ ├── compiler ------------------ web端编译相关代码,将 template 编译为 render 函数
│ │ │ ├── runtime ------------------- web端运行时相关代码,用于创建Vue实例等
│ │ │ ├── server -------------------- 服务端渲染
│ │ │ ├── util ---------------------- 相关工具类
│ │ │ ├── entry-runtime.js ---------- 运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
│ │ │ ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
│ │ │ ├── entry-compiler.js --------- vue-template-compiler 包的入口文件
│ │ │ ├── entry-server-renderer.js -- vue-server-renderer 包的入口文件
│ │ │ ├── entry-server-basic-renderer.js -- 输出 packages/vue-server-renderer/basic.js 文件
│ │ ├── weex -------------------------- 混合应用
│ ├── server ---------------------------- 包含服务端渲染(ssr)的相关代码
│ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│ ├── shared ---------------------------- 全局共享的方法和常量
├── test ---------------------------------- 包含所有测试文件
├── types --------------------------------- 支持TypeScript,TypeScript类型声明文件
├── .babelrc ------------------------------ babel 配置文件
├── .editorconfig ------------------------- 针对编辑器的编码风格配置文件
├── .eslintignore ------------------------- eslint 忽略配置
├── .eslintrc ----------------------------- eslint 配置文件
├── .flowconfig --------------------------- flow 的配置文件
├── .gitignore ---------------------------- git 忽略配置
├── .BACKERS.md --------------------------- 赞助者信息文件
├── LICENSE ------------------------------- 项目开源协议
├── package.json -------------------------- 依赖
├── README.md ----------------------------- 说明文档
├── yarn.lock ----------------------------- yarn 锁定文件
对于目录结构并不需要在看到描述后就完完全全知道所有文件的作用,刚开始知道个大概,在源码解析的过程中慢慢去理解体会。
Vue 不同版本的构建
在 dist 输出文件夹的 REMEDE.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 |
其中行上按照输出的模块形式分为 UMD、CommonJS、ES Module 三种,而列上按照环境和版本分别分成 Full、Runtime-only、Full (production)、Runtime-only (production)四种。作者按照这些分类在 dist 文件夹下构建了多个不同的文件以供使用。下面简单解释一下这些分类。
- UMD: UMD 版本可以通过 <script> 标签直接用在浏览器中。
jsDelivr CDN 的 https://cdn.jsdelivr.net/npm/vue 默认文件就是运行时 + 编译器的 UMD 版本 (vue.js)。
- CommonJS:CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。
这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js) —— 对应 package.json 的 main 。
- ES Module: 为打包工具提供的 ESM:为诸如 webpack 2 或 Rollup提供的现代打包工具。
ESM格式被设计为可以被静态分析, 所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js) —— 对应 package.json 的 module 。
- ES Module(2.6+):为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。
如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器
Full: 完整版 = 运行时版 + Compiler(编译器)
Runtime-only:运行时版
Full (production)、Runtime-only (production): 生产环境的完整版和运行时版
为什么要分 运行时版 与 完整版?完整版比运行时版多了一个 Compiler,Compiler在目录结构中解释为 将 template 编译为 render 函数,这个过程不一定要在代码运行的时候去做,实际上可以在构建的时候完成,这样真正运行的代码就免去了这样一个步骤,提升了性能。同时,将 Compiler 抽离为单独的包,还减小了库的体积。完整版是允许在代码运行的时候去现场编译模板,在不配合构建工具的情况下可以直接使用。我们更多时候配合构建工具使用运行时版本。
同样,我们在 scripts/config.js(Rollup配置文件)中也能看出这些分类的区别。
不同模块的运行时和完整版入口文件是一样的。运行时的入口文件名字为 entry-runtime.js,完整版的入口文件名字为 entry-runtime-with-compiler.js(web 是 alias 中配置的别名: src/platforms/web)。但是输出的格式(format)是不同的,分别是 cjs、es 以及 umd。
对应 package.json 文件中的启动命令也不同。
"scripts": {
// 构建完整版 umd 模块的 Vue
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
// 构建运行时 cjs 模块的 Vue
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs",
// 构建运行时 es 模块的 Vue
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
// 构建 web-server-renderer 包
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
// 构建 Compiler 包
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
}
Vue 构造函数
在使用 Vue 的时候,要使用 new 操作符进行调用,说明 Vue 应该是一个构造函数。可以从 npm run dev 作为切入点。
在 package.json 文件中
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
根据 scripts/config.js 文件中的配置
// 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
},
找到入口文件为 src/platforms/web/entry-runtime-with-compiler.js,在这个文件中可以看到 vue 是从另一个文件中引用的。
import Vue from './runtime/index'
打开 ./runtime/index.js 文件,vue 同样是从另外一个文件中引用的。
import Vue from 'core/index' // core 是 alias 中配置的别名: src/core
打开 src/core/index.js 文件,
import Vue from './instance/index'
打开 ./instance/index.js 文件, 发现 Vue 构造函数是在这里被定义的。
// 从五个文件导入五个方法(不包括 warn)
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// 定义 Vue 构造函数
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
// 使用了安全模式来提醒要使用 new 操作符来调用 Vue
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 将 Vue 作为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
// 导出
export default Vue
其中最主要的是导入的 initMixin、stateMixin、renderMixin、eventsMixin、lifecycleMixin 五个方法,依次看一下。
打开 ./init.js 文件,找到 initMixin 方法,
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 函数体
}
}
可以看到这个方法的作用就是在 Vue 的原型上添加 _init 方法,当我们执行 new Vue() 的时候,this._init(options) 将被执行。顾名思义,应该是初始化某些东西。
再打开 ./state.js 文件,找到 stateMixin 方法,
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
//如果不是生产环境,就为 $data 和 $props 这两个属性设置 set 只读
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 函数体
}
}
可以看到,stateMixin 先使用 Object.defineProperty 在 Vue.prototype 上定义了 $data 和 $props 两个属性,又在 Vue.prototype 上定义了 $set、$delete 以及 $watch三个方法。其中,$data 和 $props 这两个属性的定义分别写在了 dataDef 以及 propsDef 这两个对象里,通过 get 方法代理了 _data 和 _props 这两个实例属性。
再打开 ./events.js 文件,找到 eventsMixin 方法,
export function eventsMixin (Vue: Class<Component>) {
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
}
在 Vue.prototype 上定义了 $on、$once、$off 以及 $emit 四个方法。
再打开 ./lifecycle.js 文件,找到 lifecycleMixin 方法。
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
}
在 Vue.prototype 上定义了_update、$forceUpdate 以及 $destroy 三个方法。
最后打开 render.js 文件,找到 renderMixin 方法,
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
}
renderMixin 方法先执行 installRenderHelpers 函数,又在 Vue.prototype 上添加了 $nextTick 和 _render 两个方法。
而 installRenderHelpers 函数在 render-helpers/index.js 文件中,打开后,
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
可以发现这个函数的作用是在 Vue.prototype 上添加一系列方法。
总的看来,在 ./instance/index.js 文件中的五个方法其实就是在 Vue.prototype 上挂载一些属性和方法。这也是对 Vue 构造函数的一个初探。