从在基于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 = setVue.delete = delVue.nextTick = nextTick// 2.6 explicit observable APIVue.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 installationObject.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 utilsVue.config.mustUseProp = mustUsePropVue.config.isReservedTag = isReservedTagVue.config.isReservedAttr = isReservedAttrVue.config.getTagNamespace = getTagNamespaceVue.config.isUnknownElement = isUnknownElement
因为现在的运行环境已经变成了web平台,所以一些全局配置就不能再粗暴的直接给个默认值,而是要具体问题具体分析了,比如这isReservedTag
,默认值是no的它,就要被重写成根据tag返回一个布尔值:
export const isReservedTag = (tag: string): ?boolean => { return isHTMLTag(tag) || isSVG(tag)}
安装平台的指令和组件
// install platform runtime directives & componentsextend(Vue.options.directives, platformDirectives)extend(Vue.options.components, platformComponents)
其中,web平台的指令有model
和show
两个,组件有Transition
和TransitionGroup
两个,这些都和dom息息相关。
安装平台补丁函数
// install platform patch functionVue.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 methodVue.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.$mountVue.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过程时有的放矢。