聊一聊vue的双向数据绑定,为啥子this.message和this._data.message 都能访问到呢?都发生了什么呢

26次阅读

共计 4413 个字符,预计需要花费 12 分钟才能阅读完成。

初入 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,望各位大神批评指正,祝大家学习愉快!

正文完
 0