大家都知道,阅读源码可以帮助自己成长。源码解析的文章也看了不少,但是好记性不如烂笔头,看过的东西过段时间就忘的差不多了,所以还是决定自己动手记一记。
首先看下项目目录,大致知道每个文件夹下面都是干什么的
Vue.png
当我们阅读一个项目源码的时候,首先看它的package.json文件,这里包含了项目的依赖、执行脚本等,可以帮助我们快速找到项目的入口。
我们来看几个重要字段:
// main和module指定了加载的入口文件,它们都指向运行时版的Vue,
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
当打包工具遇到该模块时:
- 如果已经支持pkg.module字段,会优先使用es6模块规范的版本,这样可以启用tree shaking机制
- 否则根据main字段的配置加载,使用已经编译成CommonJS规范的版本。
webpack2+和rollup都已经支持pkg.module, 会根据module字段的配置进行加载
接下来看一下scripts里面部分脚本配置:
"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-dev",
// 构建运行时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",
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer"
},
umd让我们可以直接用script标签来引用Vue
cjs形式的模块是为browserify 和 webpack 1 提供的,他们在加载模块的时候不能直接加载ES Module
webpack2+ 以及 Rollup可以直接加载ES Module,es形式的模块是为它们服务的
接下来,我们将基于dev脚本进行分析
当我们执行npm run dev命令时,
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
可以看到配置文件是scripts/config.js,传给配置文件的TARGET变量的值是‘web-full-dev’。
在配置文件的最后,是这样一段代码:
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
因为process.env.TARGET有值,所以执行的是if里面的代码。根据process.env.TARGET === 'web-full-dev', 我们看到这样一段配置:
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
dest: resolve('dist/vue.js'), // 最终输出文件
format: 'umd', // umd模块
env: 'development',
alias: { he: './entity-decoder' },
banner
},
现在我们知道了入口文件是'web/entry-runtime-with-compiler.js',但是web是指的哪一个目录呢?在scripts下面有一个alias.js文件,里面定义了一些别名:
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
entries: resolve('src/entries'),
sfc: resolve('src/sfc')
}
可以看到web是指的'src/platforms/web',所以入口文件的全路径就是src/platforms/web/entry-runtime-with-compiler.js
我们使用Vue的时候,是用new
关键字进行调用的,这说明Vue是一个构造函数,接下来我们就从入口文件开始扒一扒Vue构造函数是咋个情况。
寻找Vue构造函数的位置
打开入口文件src/platforms/web/entry-runtime-with-compiler.js
,我们看到这样一句代码
import Vue from './runtime/index'
这说明Vue是从别的文件引进来的,接着打开./runtime/index
文件,看到
import Vue from 'core/index'
说明这里也不是Vue的出生地,接着寻找。打开core/index
,根据别名配置可以知道,core是指的'src/core'目录。Vue依然是引入的
import Vue from './instance/index'
没办法,接着找。在./instance/index
下面看到这样一段代码
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)
}
长吁一口气,Vue构造函数终于找到源头了。最后我们再理一下这个路径
src/platforms/web/entry-runtime-with-compiler.js.
——> src/platforms/web/runtime/index.js
——> src/core/index.js
——> src/core/instance/index.js
接下来我们从出生地开始一一来看
Vue构造函数——实例属性和方法
来看一下src/core/instance/index.js
文件中的全部代码:
/**
* 在原型上添加了各种属性和方法
*/
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)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
// 在Vue的原型上添加了_init方法。在执行new Vue()的时候,this._init(options)被执行
stateMixin(Vue)
// 在vue的原型上定义了属性: $data、$props,方法:$set、$delete、$watch
eventsMixin(Vue)
// 在原型上添加了四个方法: $on $once $off $emit
lifecycleMixin(Vue)
// 在Vue.prototye上添加了三个方法:_update $forceUpdate $destory
renderMixin(Vue)
// 在原型上添加了方法:$nextTick _render _o _n _s _l _t _q _i _m _f _k _b _v _e _u _g _d _p
export default Vue
该文件主要是定义了Vue构造函数,然后又以Vue为参数,执行了initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin这五个方法。
Vue构造函数首先检查了是不是用new
关键字调用的,然后调用了_init方法。
接下来五个方法分别在Vue的原型上添加了各种属性和方法。首先来看initMixin
initMixin
打开'./init'文件,找到initMixin方法,发现它其实只做了一件事:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
}
}
就是在Vue.prototype上挂载了_init方法,在执行new Vue()的时候,该方法会执行。
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') { // 不是生产环境,设置set
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
// $data 和 $props是只读属性
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 {
...
}
这个方法首先在Vue.prototype上定义了两个只读属性$data
和$props
。为什么是只读属性呢?因为为属性设置set的时候有一个判断,不能是生产环境。
然后在原型上定义了三个方法:$set
, $delete
, $watch
eventsMixin
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
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 {}
}
这里面是在原型上挂载了四个方法,这几个方法平时也都经常用到,肯定很熟悉
lifecycleMixin
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
}
添加了三个生命周期相关的实例方法:
- _update:
- $forceUpdate: 迫使Vue实例重新渲染,包括其下的子组件
- $destory: 完全销毁一个实例, 触发生命周期beforeDestroy和destroyed
renderMixin
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {}
}
首先是以Vue.prototype为参数调用了installRenderHelpers方法,来看一下这个方法干了啥:
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
}
也是在原型上挂载了各种方法, 用于构造render函数。
之后又在原型上挂载了两个实例方法$nextTick
和_render
至此我们大致了解了instance/index.js
里面的内容,就是包装了Vue.prototyp,在其上挂载了各种属性和方法。
Vue构造函数——挂载全局API
接下来来看/src/core/index
文件
/**
* 添加全局API,在原型上添加了两个属性$isServer和$ssrContext,加了version版本属性
*/
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 在 Vue 构造函数上添加全局的API
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
// 存储了当前Vue的版本号
Vue.version = '__VERSION__'
export default Vue
首先是导入了构造函数Vue和其他三个变量,接下来就是以Vue构造函数为参数调用了initGlobalAPI方法,该方法来自./global-api/index
。我们先把下面的内容看完再回过头来分析该方法。
接下来是在Vue.prototype上面挂载了两个只读属性$isServer
和$ssrContext
。之后又在Vue构造函数上添加了FunctionalRenderContext属性,根据注释知道该属性是在ssr中用到的。
最后在Vue构造函数上添加了静态属性version,其值是__VERSION__
,这是个什么鬼?打开/scripts/config.js
,可以看到这么一句代码:
__VERSION__: version
而version的值在文件最上面可以看到:
process.env.VERSION || require('../package.json').version
所以最终的值就是Vue的版本。
我们再回过头来看一下initGlobalAPI函数,从函数名可以猜出它应该是定义全局API的,其实也就是这样。
先看前部分代码
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef) // 只读属性
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 上面意思就是轻易不要用,有风险
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
先是定义了只读属性config。接着定义了util属性,并且在util上挂载了四个方法。只不过util以及它下面的方法不被视为公共API的一部分,要避免使用,除非你可以控制风险。
接着就是在Vue上添加了四个属性:set、delete、nextTick、observable.
然后定义了一个空对象options
Vue.options = Object.create(null)
之后通过循环填充属性:
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
ASSET_TYPES的值通过查找对应文件后知道为['component', 'directive', 'filter'],所以循环之后options对象变为:
Vue.options = {
components: Object.create(null),
directives: Object.create(null),
filters: Object.create(null)
}
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
这是在options上添加了_base属性
接下来是这句代码
// 将builtInComponents的属性混合到Vue.options.components中
extend(Vue.options.components, builtInComponents)
extend 来自于 shared/util.js 文件,代码也很简单
/**
* Mix properties into target object.
*/
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
builtInComponents 来自于 core/components/index.js 文件
import KeepAlive from './keep-alive'
export default {
KeepAlive
}
现在为止,Vue.options变成
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
在函数的最后,调用了四个方法:
// 在Vue构造函数上添加use方法,Vue.use()用来安装Vue插件
initUse(Vue)
// 添加全局API:Vue.mixin()
initMixin(Vue)
// 添加Vue.cid静态属性 和 Vue.extend 静态方法
initExtend(Vue)
// 添加静态方法:Vue.component Vue.directive Vue.filter
// 全局注册组件、指令、过滤器
initAssetRegisters(Vue)
我们先大致了解这几个方法的作用,至于具体实现以后再详细分析。
第二个阶段大体就了解完了,就是挂载静态属性和方法。
Vue平台化包装
接下来来看platforms/web/runtime/index.js
文件,我们之前看的两个文件是在core目录下的,是Vue的核心文件,与平台无关的。platforms下面的就是针对特定平台对Vue进行包装。主要分两个平台:web和weex, 我们看的是web平台下的内容。
首先是安装特定平台的工具函数
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
Vue.config我们之前见过,它代理的是/src/core/config.js
文件抛出的内容,现在是重写了其中部分属性。
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
这是安装平台运行时的指令和组件。extend的作用我们都已经知道了。来看一下platformDirectives和platformComponents的内容。
platformDirectives:
import model from './model'
import show from './show'
export default {
model,
show
}
platformComponents:
import Transition from './transition'
import TransitionGroup from './transition-group'
export default {
Transition,
TransitionGroup
}
Vue.options之前已经有过包装,经过这两句代码之后变成:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
继续看下面的代码
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
这是添加了两个实例方法:__patch__
和 $mount
看完之后我们就知道了该文件的作用
- 设置平台化的Vue.config
- 在Vue.options上混合了两个指令:
model
和show
- 在Vue.options上混合了两个组件:
Transition
和TransitionGroup
- 在Vue.prototye上添加了两个方法:
__patch__
和$mount
compiler
到目前为止,运行时版本的Vue已经构造完了。但是我们的入口是entry-runtime-with-compiler.js
文件,从文件名可以看出来这里是多了一个compiler。我们来看看这个文件吧
// 获取拥有指定ID属性的元素的innerHTML
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( // 重写了$mount方法
el?: string | Element,
hydrating?: boolean
): Component {}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
// 添加compile全局API
Vue.compile = compileToFunctions
export default Vue
这个文件主要是重写了Vue.prototype.$mount
方法,添加了Vue.compile
全局API
以上,我们从Vue构造函数入手,大致梳理了项目的脉络。理清楚了大体流程,之后再慢慢探索细节。
网友评论