初入vue 的大门,对vue 在data 中定义一个值,在DOM 中通过双花括号{{}} 就可以访问的到,对此我是很好奇的,本着好奇的心思,打开了vue 的源码,探索这个奇妙的过程。
问题的根源就在于这个方法:
Object.defineProperty()定义对象的属性相关描述符, 其中的set和get函数对于完成数据双向绑定起到了至关重要的作用。
下面我们就通过一个简单的例子来解释一个这个方法:
var obj = {
name: "south Joe"
}
Object.defineProperty(obj, 'foo', {
get: function () {
console.log('将要读取obj.name属性');
},
set: function (newVal) {
console.log('当前值为', newVal);
}
});
obj.name; // 将要读取obj.foo属性
obj.name = 'yangyang'; // 当前值为 name
那么解释完这个属性之后呢,我们就深入vue 源码,了解一下在源码背后做了一些什么呢?
我们调用Vue,并传入一个data;
import Vue from 'vue'
new Vue({
el: '#app',
mounted() {
console.log(this.message);
console.log(this._data.message);
},
data() {
return {
message: 'hello vue'
}
}
})
我们实例化一个Vue 的实例,并传入了一个对象;
调用了源码中的这个方法:
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');
}
// 执行_init 方法;_init 是Vue 原型上的方法,该函数在调用initMixin 的时候,在Vue的原型上定义的;同时传入options;
this._init(options);
}
在_init 方法中,做了一堆初始化的操作; 定义uid,合并options,我们这里主要看一个方法initState(vm);
Vue.prototype._init = function (options) {
debugger
var vm = this;
// a uid
vm._uid = uid$3++;
var 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);
}
};
在initState 中我们初始化了一个initData();
function initState (vm) {
vm._watchers = [];
var 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);
}
}
看到这里,大家是不是有些着急了呢,关键的地方马上就要来了,请大家耐心。在initData中我们主要获取到data 中的对象,data 可以是一个对象,也可以是一个函数;还在里面做了一个代理proxy(vm, _data
, key)
function initData (vm: Component) {
// 拿到¥options 上的data; data 可以是一个函数,但也可以是一个对象;
let data = vm.$options.data
// 这里的赋值导致可以通过this._data.message
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
// 拿到对象的key;data,props,method 不能重名,因为他们最终都会挂载到vm上,当前这个实例上,那么是怎么实现的,就是通过proxy(vm, `_data`, key);
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') {
// hasOwn 判断对象里面是不是又这个key
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 顾名思义就是一层代理,
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式处理;
observe(data, true /* asRootData */)
}
将数据代理到实例上
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
// 通过一个对象定义了一个get,和一个set;
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 通过Object.defineProperty 代理了这个target,key ,对target,key 的访问做了一层get,set;
// target 就是vm, key 就是_data
// 当我们访问this.message ,就是访问this._data.message,执行的是get方法;
// 不要通过这种方式去访问,this._data.message, 下划线在编程界默认是私有属性;
Object.defineProperty(target, key, sharedPropertyDefinition)
}
当我们执行this.message 的时候,就会调用get 方法,当我们给this.message 赋值的时候就会调用set方法;
到这里,大家是不是就很清楚vue 的双向数据绑定了呢?
职场小白south Joe,望各位大神批评指正,祝大家学习愉快!