从在基于Vue-Cli
的项目中,我们在main.js
一般是这样使用Vue的
import Vue from 'vue';
import router from './router';
import store from './store';
import App from './App.vue';
new Vue({
el: '#app',
router,
store,
render: h => h(App)
});
我们new的这个Vue倒是是个啥?log看一下
所以我们new
的是一个Vue
对象的实例,它包含了图上那些属性和方法。那么这些实例上的属性和方法又是再哪里加上的呢?
我们在new Vue
的时候用chrome
打个断点,用下面这个step into next function call
的工具看看这个new Vue
到底调用了什么方法
构造函数
我们首先通过全局搜索function Vue
,我们找到真正Vue
的构造函数,在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'
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)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
其实vue
的构造函数,就做了一件事,执行自己的_init
方法。但在执行init之前,我们log个一下这个Vue实例看看:
……
console.log(this);
this._init(options)
……
怎么就突然冒出来这么多奇奇怪怪的东西?这些在_init
之前就存在的属性到底是什么时候加到我们这个Vue
的原型上的?
各种mixin
首先我们把怀疑的目光放在下面这些mixin
上,毕竟我们的_init
既然没有在function Vue
这个构造函数中申明,那肯定是从哪里加到原型上的。
initMixin
我们先来看看initMixin执行前后,Vue原型上的变化
console.log(Vue.prototype)
initMixin(Vue)
console.log(Vue.prototype)
所以,实际上initMinxin
就在Vue的原型上挂了一个构造函数需要执行的_init
方法。通过initMinxin
函数的源码我们也可以印证这一点:
export function initMixin (Vue: Class<Component>) {
// 对Vue扩展,实现_init实例方法
Vue.prototype._init = function (options?: Object) {
……
}
}
stateMixin
console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)
stateMinix里做的都是一些跟响应式相关的勾当,从上图可以看到他们是$data
,$props
两个属性;$set
,$delete
,$watch
三个方法。源码如下:
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') {
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 {
……
}
}
通过源码可以知道$data
,$props
是只读属性,是通过Object.defineProperty
来实现的。举个简单的例子,比如我们要直接暴力去修改$props
:Vue.prototype.$props = a;
这时候就会触发propsDef
的set
方法,会警告说$props is readonly
。
eventMixin
console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)
eventMinix里做的都是一些事件相关的东西,从上图可以看到,挂载了$on
,$once
,$off
,$emit
四个函数。
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
从源码上我们可以看到,这几个函数其实都是在对vm实例上的_events这个数组在进行操作。而$on
和$once
的区别也很清晰,$once
的原理其实就是对$on
执行的函数进行了封装,这个函数执行前会先将自己$off
,从而达到只执行一次的目的。
lifecycleMixin
console.log(Vue.prototype)
lifecycleMixin(Vue)
console.log(Vue.prototype)
从上图可知,lifecycleMinix里并不是我们想象中的那些生命周期的钩子函数,他挂载了_update
,$forceUpdate
,$destroy
这三个函数。
export function lifecycleMixin (Vue: Class<Component>) {
// 更新函数
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
……
}
// 强制更新
Vue.prototype.$forceUpdate = function () {
……
}
// 销毁
Vue.prototype.$destroy = function () {
……
}
}
renderMixin
console.log(Vue.prototype)
renderMixin(Vue)
console.log(Vue.prototype)
从上图可以知道,renderMixin里,做的事情就比较多了,除了$nextTick
,_render
这两个函数,installRenderHelpers
方法还挂载了很多在render
过程中需要用到的工具函数。
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 {
……
}
}
我们看一下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
}
来自上层的封装
在执行完上面5个minxin
方法后,最后log
出来的Vue
原型上,与我们在this._init()
执行之前所看到的Vue实例上的属性和方法还是有少了一些,比如$mount
这个函数,这些东西又是啥时候加到原型上的呢?
既然当前这个/src/core/instance/index.js
文件没有线索了,而且他最后还把Vue
这个构造还是export
了出去,所以我们需要看看有没有文件在外面import
这个Vue
,然后再加上一些骚操作。
我们搜索import Vue from
这个关键字,除了test目录下的测试代码外,我们发现还有几个文件在import Vue
core/index
initGlobalAPI
在core/index里,首先会去初始化化全局API,即执行initGlobalAPI(Vue)
这个函数,这个函数会给我们的Vue原型和构造函数里加很多东西。
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)
首先实在构造函数上加上了一个只读属性config,这个config里就是Vue的全局配置项
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
然后就是util,里面包含了mergeOptions
(来自src/core/util/options.js),defineReactive
(来自src/core/observer/index.js),extend
,(来自src/shared/util.js)warn
(来自src/core/util/debug.js)
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
接下来给Vue构造函数上加入了set
,delete
方法(来自core/observer/index.js),nextTick
方法(来自src/core/util/next-tick.js),而observable
实在observe
(来自core/observer/index.js)基础上进行了封装。
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
然后就是在Vue构造函数上加入option对象,里面有components
,directives
,filters
和_base
。然后这个extend的作用是将keep-alive这个组件加入到上面的这个Vue.options.components
中。
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
最后就是初始化一下use
,minix
,extend
。其中,在initExtend的时候,会给我们的根组件构造器加上唯一cid=0,以后通过Vue.extend构造组件实例的时候,也会给每个实例的构造器加上这个递增的cid。然后再在用最后的initAssetRegisters做一次代理,把options里面的components
,directives
,filters
直接挂载到Vue的构造函数下面。
环境相关
接下来,在core/index里,去定义环境相关的一些属性。在Vue原型上定义了$isServer
,$ssrContext
,看名字都知道是与服务端渲染ssr相关的东西。最后定义在构造函数上一个与函数式组件渲染上下文相关的FunctionalRenderContext
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.version = '__VERSION__'
最后会在构造函数上加上我们的版本信息,在webpack打包的时候会替换成当前Vue的版本const version = process.env.VERSION || require('../package.json').version
platforms/web/runtime/index
Vue.js 最初是为 Web 平台设计的,虽然可以基于 Weex 开发原生应用,但是 Web 开发和原生开发毕竟不同,在功能和开发体验上都有一些差异,这些差异从本质上讲是原生开发平台和 Web 平台之间的差异。因此,在platforms/web/runtime/index这一层,会加入一些与平台相关的属性与操作。
重写config
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
因为现在的运行环境已经变成了web平台,所以一些全局配置就不能再粗暴的直接给个默认值,而是要具体问题具体分析了,比如这isReservedTag
,默认值是no的它,就要被重写成根据tag返回一个布尔值:
export const isReservedTag = (tag: string): ?boolean => {
return isHTMLTag(tag) || isSVG(tag)
}
安装平台的指令和组件
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
其中,web平台的指令有model
和show
两个,组件有Transition
和TransitionGroup
两个,这些都和dom息息相关。
安装平台补丁函数
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
在Vue原型上的__patch__
方法,是由一个工厂函数createPatchFunction
返回的,实际上执行的是src/core/vdom/patch.js中第700的那个patch函数。
实现$mount方法
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
在Vue原型上的$mount
方法,实际上执行的就是mountComponent
这个方法,只是对el进行了一下处理。
web/entry-runtime-with-compiler.js
这个文件是webpack编译的入口文件,他主要做了两件事,第一是扩展$mount
方法,第二个是挂载compile
方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
……
// 最后执行的还是Vue.prototype.$mount
return mount.call(this, el, hydrating)
}
Vue.compile = compileToFunctions
总结
我们这一次把Vue init之前所有挂载的属性和方法都总结了一遍,目的不是为了搞清楚每个属性的意义,每个方法实现的功能(这工作量太大)。而是为后面的源码工作打下基础,知道原型和构造函数上有哪些属性和方法,又是在哪里定义的,不会看到之后一脸懵逼。然后再通过具体的流程去看每个函数,每个属性究竟有什么用。最后我用一脑图总结了一下init前Vue原型和构造函数上的属性和方法,让我们接下来去看init过程时有的放矢。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。