data中定义了一个数据msg, vue实例上访问这个数据有两种方式,this.$data.msg 和 this.msg,请问,为vue如何实现this.msg能直接访问到data中的msg变量???data又为什么是个函数?
分析:
clone 下 vue 的项目源码,然后打开 src/core/instance/index.js
调用 init 方法时先进行了检查,确保已经使用 new Vue 初始化Vue(已构造一个作用域安全的构造函数)并将options 参数传入_init 内 ,this._init 是 Vue.prototype._init ,这个init方法来自于 ‘./init.js’ 中, 是下面initMixin(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) // 判断 this 对象的 prototype 是否存在于构造函数 Vue 的原型链上
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 已构造一个作用域安全的构造函数:new Vue 进行初始化 Vue 并将options 参数传入_init 内 ,this._init 是 Vue.prototype._init ,这个init方法来自于 ‘./init.js’ 中, 是下面initMixin(Vue)增加的
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
进入 src/core/instance/init.js
此时先看这句代码 const vm: Component = this ,此处 this 即 vue实例,然后赋值给了 vm , vm将做为参数向下传递,也就是一会以下的 vm 都是指向 vue 实例
然后看代码 initState(vm) ,初始化状态(这个状态包含data props methods)
// src/core/instance/init.js
export function initMixin (Vue: Class) {
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) // 初始化render
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化data
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) // 挂载
}
}
}
进入 src/core/instance/state.js
看代码 initData(vm) 初始化 data
// src/core/instance/state.js
// 这个初始化解决了实例化是通过 this 获取 data props methods 内数据
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) { // 如果实例定义了 data
initData(vm) // 初始化 data
} else {
observe(vm._data = {}, true /* asRootData */)
}
………
}
看以下代码实现了 this.$data.xxx 和 this._data 是两份一模一样的值,虽然我们平时用的都是 vm.$data 里的值,但是看源码其实还有个 vm._data
function initData (vm: Component) {
let data = vm.$options.data // vm.$options 是访问自定属性,此处就是vue实例中的 this.$data
data = vm._data = typeof data === 'function' // 先执行三元表达式,然后赋值给 vm._data 和 data,这样子这两个值都是同时变化的,就是 this.$data.xxx 和 this._data 同时变化
? getData(data, vm)
: data || {}
………
}
我们打印一下项目中的this.$data、this._data,亲们可以看一下效果
重点来了:return data.call(vm, vm)
data 函数执行的时候 用 call 方法,让 vm 继承了 data 的属性和方法,也就是 this 继承了 this.$option.data 的属性和方法, 所以我们可以使用 this.xxx
call 知识点:call 是 Function 对象自带的一个方法,可以改变函数体内部的 this 指向,第一个参数就是 this要指向的对象,也就是想指定的上下文,后面的参数它会按顺序传递进去。它的函数会被立即调用。
官网解释:调用一个对象的一个方法,以另一个对象替换当前对象。也就是继承模式:挟持另一个对象的方法,继承另外一个对象的属性
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()
}
}
再补充一点,data 为什么是个函数而不是个对象
看以上代码,initData() 方法:先有个 三元表达式 typeof data === ‘function' ? getData(data, vm) : data || {} ,如果data是个函数则执行 getData() 方法,如果不是则返回原值,如果原值不存在则返回一个空对象,但是为什么 data 得是个函数???
这里直接说结论吧
如果 data 是个对象,那么整个vue实例将共享一份数据,也就是各个组件实例间可以随意修改其他组件的任意值,这就很坑爹了。
但是 data 定义成一个函数,将会 return 出一个唯一的对象,不会和其他组件共享一个对象。
我们注册组件的时候实际上是建立了一个组件构造器的引用,只有使用组件的时候才会真正创建一个组件实例。
那么问题来了,import引入文件时,我们一个页面A引用了3次同一个组件B,但是B组件里又引用了1次组件C,那它们各import了几次呢?
答案:import 都只执行一次,但是不止如此,但凡 export default {} 外的方法都只执行一次,但是 export default {} 对象里的方法都执行了3次。
总结:
this.$data.xxx、this._data.xxx、this.xxx 都能获取数据,先让 this.$data.xxx = this._data.xxx,然后用 call 让当前组件实例 this 继承了 this.$data 的值,也就是 getData 方法 return 出了一份用 call 改变指向将 $data 指向 this 的数据。
data 定义成一个函数,确保了各个组件实例能拥有一份只属于自己的唯一数据
历史文章: