前端基础知识储备
1.Vue源码探索之知识小储备 ——Object.defineProperty VS proxy
响应式数据/双向绑定原理
Vue数据双向绑定主要指的是:数据变化影响视图。视图变化影响数据。其中视图影响数据通过监听用户操作来得到变化得数据。所以 Vue数据双向绑定的工作主要是如何根据Data变化更新View。 简而言之, Vue要实现数据双向绑定,需要做三件事情:
- 检测到Data变化(Observer)
- 追踪收集依赖,通知变更(Dep)
- 更新View视图(Watcher)
本文先来讲解vue实现数据双向绑定的第一步:如何检测到Data的变化。
假设我们有如下这样的一个data对象
let obj = {
a: 1,
b: 2,
c: ['排序', '递归']};
我们修改obj.a = 3, 那怎样才可以监听到这个改变呢?我们很容易想到大名鼎鼎的Object.defineProperty()。Object.defineProperty() 方法会直接在一个对象上定义一个新属性。或者修改一个对象的现有属性并返回此对象
1.Object.defineProperty()
Object.defineProperty(obj,prop,descriptor)参数:【obj】是要定义属性的对象,【prop】是要定义或修改的属性的名称或Symbol.【descriptor】:要定义或修改属性的描述符
对象里目前存在的属性描述符号有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值是可写,也可以是不可写的。存取描述符是由getter函数和setter函数所描述的属性。一个描述符只能是这两者其中之一;不能同时两者
1.1数据描述符
数据描述符是一个具有值的属性,该值可能是可写的,也可能是不可写的。
它具有以下可选的键值:
configurable:表示该属性能否通过delete删除,能否修改属性的特性或者能否修改访问器属性,默认为false。
enumerable:表示该属性是否可以枚举,即可否通过for..in访问属性。默认为false。
value:表示该属性的值。可以是任何有效的Javascript 值(数值,对象。函数等)。默认为undefined。
writable:表示该属性的值是否可写,默认为false。
【栗子】:
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
result: { value: 2, writable: true, enumerable: true, configurable: true }
1.2存取描述符
存取描述符还具有以下可选键值:
get:
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。
执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
该函数的返回值会被用作属性的值。
set:
属性的 setter 函数,如果没有 setter,则为 undefined。
当属性值被修改时,会调用此函数。
该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
显然,我们只需要利用对象属性描述符里面的存取描述符,就能够检测到对象的改变。
然,我们只需要利用对象属性描述符里面的存取描述符,就能够检测到对象的改变。
显然,我们只需要利用对象属性描述符里面的存取描述符,就能够检测到对象的改变。
【栗子】:
let _value = obj.a;Object.defineProperty(obj, 'a', {
get() {
console.log('you get a number');
return _value;
},
set(newVal){
console.log('you set a number');
_value = newVal;
}});
console.log(obj.a);obj.a = 2;console.log(obj.a);
// 打印结果you get a number1you set a numberyou get a number2
我们成功检测到了obj对象中a的改变,vue2.x正式基于这一特性实现了数据双向绑定的Observer部分。趁热打铁,再多试一次,享受Object.defineProperty的乐趣吧~
【Do it again】:
let _value = obj.c;Object.defineProperty(obj, 'c', {
get() {
console.log('you get the array');
return _value;
},
set(newVal){
console.log('you change the array');
_value = newVal;
}});
console.log(obj.c);obj.c.push('二分查找');console.log(obj.c);
// 打印结果you get the array[ '排序', '递归' ]you get the arrayyou get the array[ '排序', '递归', '二分查找' ]
OMG!怎么没有打印set里的日志,可是数组确实追加进去了。难道Object.defineProperty不能监听数组的变化?没错,确实是这样,因此vue2.x在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过 重写数组 的部分方法(push,pop, shif, unshift, splice, sort, reverse)来实现响应式,这也只限制在数组的这七个方法,其他数组方法及数组的使用则无法检测到,例如如下两种使用方式:
data.c[index] = newValue;
data.c.length--;
2.数组hack
- 复制一份数组的原型方法,防止污染
const oldArrayProperty = Array.prototype;const arrayProto = Object.create(oldArrayProperty);
- 遍历,将arrayProto对象上的方法转换为观察者对象
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; methodsToPatch.forEach(methodName => {
// 往arrayProto对象中加入methodsToPatch中的属性,并执行原生的methodsToPatch对应的方法 Object.defineProperty(arrayProto, methodName, {
value () {
//监听数组变更 console.log('you change the array');
oldArrayProperty[methodName].apply(this, arguments)
}
})})
- 让数组继承arrayProto中的方法
obj.c.__proto__ = arrayProto;
好啦,见证奇迹的时刻:
console.log(obj.c);obj.c.push('二叉分法');console.log(obj.c);
结果:[ '排序', '递归' ]you change the array[ '排序', '递归', '二叉分法' ]
一个简单的数组hack就实现了,vue2.x也正是基于这种数组方法重写的原理,监听数组变更,从而实现数组类型数据的双向绑定的。
vue2.x在实现数据响应式时,需要区别对待数组和对象,因此在vue3.0,尤大大和他的团队弃用了这种方式,选择使用es6的新特性 Proxy 和 Reflect 来实现双向绑定。
3.Proxy
vue3.0基于Proxy来做数据劫持代理,可以支持原生数组的响应式。还可以直接支持新增和删除属性,使用起来十分简洁明了。
var obProxy = new Proxy(ob, {
get(target, key, receiver) {
console.log('you get:' + key);
return target[key];
},
set(target, key, value, receiver){
console.log('you set:' + key);
// target[key] = value; // Reflect.set(target, key, value)等同于上面的target[key] = value; return Reflect.set(target, key, value);
}});
我们来分别打印一下
console.log(obj.a);obj.a = 666;console.log(obj.a);
结果:1666不进入上面的get和set方法
和
console.log(obProxy.a);obProxy.a = 2;console.log(obProxy.a);
结果:you get:a1you set:ayou get:a2
结果印证:proxy是去创建一个新的代理对象,所以在改变原对象obj时,上述中的get与set中的console均未打印。
4. 对比:为什么vue3改用proxy实现数据响应式?
1.Proxy可以直接监听对象而非属性
2.Proxy 可以直接监听数组的变化
3.Proxy有多达13种拦截方法,不限于apply,ownKeys ,deleteProerty,has等等是Object.defineProperty不具备的
4.Object.defineProperty只能遍历对象属性实现监听,而Proxy返回的是一个新对象,我们可以直接操作新的对象达到目的,大大减少了遍历带来的性能开销
5.Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是新标准的性能红利