关键词:当执行new Vue
时,实际上是执行了_init
方法。_init
方法会做一堆初始化工作,首先是对options
的合并,然后是一系列 init 方法,对data
进行proxy
处理和响应式处理observe
,最后调用$mount
做挂载。
new Vue
发生了什么从入口代码开始分析,new Vue
背后发生了哪些事情。
src/core/instance/index.js
(Vue 定义)Vue 实际上就是function
实现的class
,执行new Vue
的时候执行了function
,然后执行this._init
把options
传入,this._init
是一个 Vue 原型上的方法,是在initMixin
,也就是src/core/instance/init.js
中定义的。
// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
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)
export default Vue
在原型上定义了_init
方法,也就是说走到了initMixin(Vue)
时,执行了_init
方法。
_init
方法做了很多初始化的工作,例如:
uid
options
。将传入的options
merge 到$options
上,所以可以通过$options.el
访问到代码中定义的el
,通过$options.data
访问到我们定义的data
。render
、state
),初始化结束后判断$options
有没有el
。调用vm.$mount(vm.$options.el)
进行挂载,在页面上可以看到字符串渲染到页面上。$mount
方法是整个做挂载的方法(是个重点)。export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
问:为什么在mounted(){console.log(this.message)}
可以访问到message
?
答:(1)初始化时有一个initState
,这个函数是定义在src/core/instance/state.js
中。在initState()
中,判断options
,如果定义props
则初始化props
,定义了methods
初始化methods
,定义了data
初始化data
。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
(2)在initData
中,从$options.data
中拿到data
,就是我们定义的data(){return {message:'Hello Vue!'}}
。然后判断data
是不是一个function
(正常data
是一个函数而不是对象),是函数则调用getData
。getData
中调用call
方法,返回对象,赋值给vm._data
和data
,如果不是对象就会报警告。然后拿到对象的key
、props
、methods
并进行对比,判断是否重名。为什么不能重名?因为他们最终都挂载到vm上,也就是当前实例上。实现是用proxy(vm,`_data`,key)
实现。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:/n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
(3)proxy
通过sharedPropertyDefinition
对象定义了get
和set
两个函数,运行Object.defineProperty(target, key, sharedPropertyDefinition)
方法代理了target
和key
,就是对target
和key
做了一层访问get
和set
,target
就是vm
。vm.key
的get
会执行return this[sourceKey][key]
,也就是说访问vm.key
就是会访问this[sourceKey][key]
。sourceKey
就是传入_data
,所以访问vm.message
实际上就会访问vm._data.message
,即mounted(){console.log(this.message);console.log(this._data.message)}
。
// 根据上面 proxy(vm, `_data`, key),就是把`_data`作为sourceKey传入。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}