我们平时在面试的时候,用过vue的同学,有一个问题肯定被问过,你知道双向绑定吗?知道什么是MVVM吗?
所谓的双向绑定其实就是,ui或者数据有一方做了修改,那么另外一个也会随着改变。
视图驱动数据,这个很简单,我们只要绑定事件就可以,但是数据要怎么驱动视图呢?这里就需要去对数据做监听,我们通常称之为”数据劫持“在每一次数据改变的时候,去执行更新视图的操作。
<template>
<div>
<input v-model="message">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello World'
};
}
};
</script>复制代码
Object.defineProperty
在vue2.x版本中,数据劫持是用过Object.defineProperty这个API来实现
var message = 'hello world';
const data = {};
Object.defineProperty(data, 'message', {
get() {
return message;
},
set(newVal) {
message = newVal;
}
});
data.message // 'hello world'
data.message = 'test' // 'test'复制代码
第一个参数是要定义属性的对象,第二个参数是要定义属性的名称,第三个参数是被定义的属性描述符
当我们读取或者设置被定义的属性时,就会执行get或者set方法,也就是在这两个方法中,我们实现数据劫持,可以在这里添加一些我们附加的操作,而不只是简单的读写。
那么在vue中,是怎么实现对data中所有的属性做到数据劫持的呢?其实原理都一样,我们只需遍历所有data对象中的所有属性,并对每一个属性使用Object.defineProperty劫持即可,当属性的值发生变化的时候,我们执行一系列的渲染视图的操作。下面我们看一个简单的vue数据劫持的实现。
const data = {
name: '你不知道滴前端',
age: 25,
info: {
address: '北京'
},
numbers: [1, 2, 3, 4]
};
function observerObject(target, name, value) {
if (typeof value === 'object' || Array.isArray(target)) {
observer(value);
}
Object.defineProperty(target, name, {
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
if (typeof value === 'object' || Array.isArray(value)) {
observer(value);
}
value = newVal;
}
renderView();
}
});
}
function observer(target) {
if (typeof target !== 'object' || !target) {
return target;
}
for (const key in target) {
if (target.hasOwnProperty(key)) {
const value = target[key];
observerObject(target, key, value);
}
}
}
observer(data);复制代码
遍历这个data对象,对每一个属性都使用observerObject方法进行数据劫持。
observerObject主要做的就是使用Object.defineProperty去监听传入的属性,如果target是一个对象的话,就递归执行observer,确保data中所有的对象中的所以属性都能够被监听到。当我们set的时候,去执行renderView(执行视图渲染相关逻辑)。
Object.defineProperty的问题在于,只能够作用在对象上,那么vue中,对数组是怎么实现数据劫持的呢? 只需要修改数组的原型方法,往这些方法里添加一些视图渲染的操作。
const oldArrayProperty = Array.prototype;
const newArrayProperty = Object.create(oldArrayProperty);
['pop', 'push', 'shift', 'unshift', 'splice'].forEach((method) => {
newArrayProperty[method] = function() {
renderView();
oldArrayProperty[method].call(this, ...arguments);
};
});
// 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
if (Array.isArray(target)) {
target.__proto__ = newArrayProperty;
}复制代码
上面代码就是vue2x版本数据劫持的原理实现(不是源代码)。
现在我们可以看出Object.defineProperty的一些问题
- 递归遍历所有的对象的属性,这样如果我们数据层级比较深的话,是一件很耗费性能的事情
- 只能应用在对象上,不能用于数组
- 只能够监听定义时的属性,不能监听新加的属性,这也就是为什么在vue中要使用Vue.set的原因,删除也是同理
Proxy
在vue3版本中,使用了proxy去实现对象的监听,避免了以上问题的出现,下面我们用proxy实现一个简易版本的数据劫持。在这之前,我们先来看一些基本的概念
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy对拦截操作多达13种,所以在这13中操作中,我们可以自定义很多逻辑。Proxy的出现,其实就是为了扩展对象的能力。
实现方式: new Proxy(target, handler)
target: 拦截的目标对象
handler:定义拦截的方法
下面是Proxy所拥有的拦截方法
- handler.apply
- handler.construct 拦截构造函数调用 new Example()
- handler.difineProperty
- handler.get 拦截对象属性的读取 obj.name
- handler.set 拦截对象属性的设置 obj.name = '前端的自我修养'
- handler.getOwnPropertyDwscriptor
- handler.has
- handler.ownKeys 拦截Object.keys for in 等
- handler.setPrototypeOf
- handler.isExtensible
- handler.getPropertyOf
- handler.preventExtensiions
- handler.enumerate
大家可以再MDN上查看方法具体的作用,这里我们主要讲数据劫持,所以着重使用handle.get、handle.set和handle.deleteProperty的使用
既然有了拦截,自然还有有使用,这里不得不提到Reflect,它也是JS的一个内置的对象。
Refect设计的目的主要有一下几点:
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
- 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
- 让Object操作都变成函数行为。比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
我们用get来举例
var p = new Proxy(target, {
get: function(target, property, receiver) {
}
});
Reflect.get(target, propertyKey, receiver)复制代码
Proxy数据劫持实现
function observe(target) {
if (typeof target !== 'object' || target == null) {
return target;
}
const obseved = new Proxy(target, {
get(target, key, receiver) {
return observe(Reflect.get(target, key, receiver);)
},
set(target, key, value, receiver) {
if (value === target[key]) {
return true;
}
const ownKeys = Reflect.ownKeys(target);
if (ownKeys.includes(key)) {
console.log('旧属性');
} else {
console.log('新添加的属性');
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
return result Reflect.deleteProperty(target, key);
}
});
return obseved;
}
const data = {
name: '你不知道的前端',
age: 25,
info: {
city:'beijing'
},
numbers: [1, 2, 3, 4]
};
const proxyData = observe(data复制代码
从上面的代码中可以看出,proxy在实现数据劫持时,具有以下优点:
- proxy可以直接监听数组的修改
- proxy可以直接监听属性的新增和删除
- 在实现深度监听的时候,只有在data对象的属性被访问的时候,才去对这个属性做监听处理,而不是一次性递归所有的。
以上就是一个vue实现数据劫持的简单实例,只是在原理层面给大家展示出来,具体的源码实现上,还是有很多细节的处理,大家看完本文有了一个大概的思路后,可以去阅读下源码,对我们的理解框架的能力会有很高的提升,这些提升也会体现到日常的开发中去。