从在基于 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 过程时有的放矢。