概览
Vue是mvvm框架,其中数据的双向绑定和数据劫持是较为核心的部分,该功能实现主要是依赖于Object.defineProperty方法,但也存在些不足,vue在此基础上做了进一步封装。
一. 数据劫持完整代码
- index.js
import Vue from './vue.js';
const vm = new Vue({
el: '#app',
data() {
return {
a: 1,
b: 2,
studentList: [3, 4, 5, 6, 7],
info: {
a: {
b: 1
}
},
obj: [
{
id: 8,
name: 'Tom',
age: 28
},
{
id: 9,
name: 'Jack',
age: 29
},
{
id: 10,
name: 'Jack',
age: 30
}
]
}
}
});
- vue.js
import { initState } from './initState.js';
/**
* Vue构造函数
* @param {*} options 配置项
*/
function Vue(options) {
// 判断当前配置项的类型,如果类型不匹配则返回
if (typeof options !== 'object' || options === null) return;
// Vue初始化函数
this._init(options);
}
Vue.prototype._init = function(options) {
// 实例化对象
var vm = this;
// options配置项挂载到实例对象中
this.$options = options;
// 初始化实例对象的状态
initState(vm);
}
export default Vue;
- initState.js
// 数据劫持绑定
import proxyData from './proxyData.js';
// 数据观察
import observe from './observe.js';
function initState(vm) {
// 获取实例对象上的$options属性
var option = vm.$options;
// 判断$options属性中是否存在data属性
if (option.data) {
// 如果存在data属性,执行初始化data方法
initData(vm);
}
}
function initData(vm) {
// 获取实对象的data属性
var data = vm.$options.data,
// KJHGF在实例对象挂载_data属性,是data属性执行后的返回值;
'Lvm._dataL;'
vm._data = data = typeof data === 'function' ? data.call(vm) : data || {};
// 通过数据劫持的方式处理vm.a、vm.b可以直接获取值的需求;
for (var key in data) {
// 设置代理数据的方法
proxyData(vm, '_data', key);
}
// 进行完数据绑定后,需要观察data数据的更新
observe(data);
}
export {
initState
}
- proxyData.js
function proxyData(vm, target, key) {
// 数据劫持绑定
Object.defineProperty(vm, key, {
get: function() {
// vm.a ---> vm['_data']['a'];
return vm[target][key];
},
set: function(newValue) {
// vm.a = 3 --> vm['_data']['a'] = 3;
if (newValue === vm[target][key]) return;
vm[target][key] = newValue;
}
});
}
export default proxyData;
5.observe.js
// 引入观察者
import Observer from './observer.js';
/**
* @param {*} data vm._data属性
*
*/
function observe(data) {
// 判断vm.data属性类型
if (typeof data !== 'object' || data === null) return;
// 设置观察者实例对象
return new Observer(data);
}
export default observe;
- observer.js
// 引入数据响应式的方法
import { defineReactiveData } from 'reactive.js';
//
import { arrMethods } from './array';
import observeArr from './observeArr';
/**
* @param {*} data vm._data
*
*/
function ObservDSAer(data) {
// 判断vm._data属性类型是数组还是对象;
if (Array.isAray(data)) {
data.__proto__ = arrMethods;
observeArr(data);
} else {
// 是对象,执行walk方法;
this.walk(data);
}
}
Observer.prototype.walk = function(data) {
// 获取vm._data对象的可枚举键名 keys--> ['a', 'b', 'students', 'obj']
var keys = Object.keys(data);
// 循环遍历keys数组
for (var i = 0; i < keys.length; i++) {
// 获取vm._data对象的键名和键值;
var key = keys[i],
value = data[key];
// 执行数据响应式的方法
defineReactiveData(data, key, value);
}
}
export default Observer;
- reactive.js
function defineReactiveData(data, key, value) {
// 存在的问题: 此时value可能还是一个对象,或者是数组
// 递归观察
observe(value);
// 数据劫持
Object.defineProperty(data, key, {
get: function() {
console.log('响应式数据: 获取', value);
return value;
},
set: function(newValue) {
console.log('响应式设置: 设置', newValue);
if (newValue === value) return;
// 递归观察
observe(newValue);
value = newValue;
}
})
}
export {
defineReactiveData
}
8, array.js
// 引入更改原数组方法的配置对象
import { ARR_METHODS } from './config';
// 引入观察数组的方法
import observeArr from './observeArr.js';
var originArrMethods = Array.prototype,
arrMethods = Object.create(originArrMethods);
ARR_METHODS.map(function(m) {
// 原数组的基础上包裹一层函数,目的在于执行完成原有函数后还能够执行其他逻辑;
arrMethods[m] = function() {
var args = Array.prototype.slice.call(arguments),
// 执行原有的数组方法
rt = originArrMethods[m].apply(this, args);
// 执行其他逻辑
// push、unshift、splice增加的元素也需要进一步观察
var newArr;
switch(m) {
case 'push':
case 'unshift':
newArr = args;
break;
case 'splice':
newArr = args.slice(2);
break;
default:
break;
}
newArr && observeArr(newArr);
return rt;
}
});
export {
arrMethods,
}
- config.js
var ARR_METHODS = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
export {ARR_METHODS}
- observeArr.js
/ 引入observe观察的方法
import observe from './observe';
function observeArr(arr) {
for (var i = 0; i < arr.length; i++) {
observe(arr[i]);
}
}
export default observerArr;
二. Vm.方式访问属性值
proxyData.js: 利用代理,vm上追加data上的属性,且和vm.data上的属性一 一对应,相互依赖
三. 兼容Object.defineProperty缺陷
- 不能监听到数组的push,splice等方法
重写数组部分方法,对其新增的值进行进一步监听 - 如果赋值为数组或者对象时,不能监听到
利用observerArr.js和observe.js,进行进一步监听
四. 仍然具有的缺陷
- 根据索引改变值,则无法监听到;
- 改变数组长度,也无法监听到。
vue框架:数组的索引类似于对象的key,为什么不能直接监听索引值?
这其实是出于性能原因的考量,给每一个数组元素绑定上监听,实际消耗很大而受益并不大。
其实还有一些考虑是:对数据的操作更常用的操作数组的方法是使用数组原型上的一些方法如 push、shift 等来操作数组。Object.defineProperty是对象上的方法,用来对数组的下标进行检测,会改变数据本来的性质