1. Object.defineProperty
1.1 监听特定属性
const pager = { pageSize: 50 };
Object.defineProperty(PAGER, 'pageIndex', {
get: function () {
return this._value;
},
set: function (v) {
this._value = v;
loadCurPageData(v); // 监听到pageIndex属性设置的时候去做其他相关操作
}
});
1.2 监听对象每个已有属性
const pager = {
pageIndex: 0,
pageSize: 50,
total: 0
};
Object.keys(pager).forEach(key => {
let val = pager[key];
Object.defineProperty(pager, key, {
get: function () {
return val;
},
set: function (v) {
val = v;
console.log('设置属性:', key, v);
}
});
});
1.3 拦截方法的调用
let hasHeaderCondition = false;
// mainGrid对象有个clearFilter方法,我需要在它调用这个方法之后做点自己的逻辑
let fnOnClear = mainGrid.clearFilter;
Object.defineProperty(mainGrid, 'setFocusFilter', {
value() { // 重新定义value,在内部执行一下原方法,再做自己的逻辑
fnOnFilter.apply(this, arguments);
hasHeaderCondition = true;
}
});
1.4 拦截数组方法的调用
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(methodName => {
Object.defineProperty(arrayMethods, methodName, {
value: function () {
var original = arrayProto[methodName];
var result = original.apply(this, arguments); // 先执行原方法
// 做一些自己想做的逻辑
return result;
}
})
});
1.5 深层对象
// 需要递归监听每个对象属性值
function defineReactive(object) {
if (!object || typeof object !== 'object') {
return;
}
let keys = Object.keys(object);
for (let key of keys) {
let value = object[key];
Object.defineProperty(object, key, {
set: function (v) {
if (v === value) { return; }
value = v;
console.log('对象修改', key, v);
defineReactive(v); // 新属性值继续设置监听
},
get: function () {
return value;
}
});
defineReactive(value); // 属性值继续设置监听
}
}
1.6 Object.defineProperty
的缺点
- 一次只能监听一个属性,如果需要监听所有属性则需要枚举所有属性名去遍历
- 如果是深层对象的话,一开始就需要深度遍历去监听所有属性
- 无法监听属性的新增和属性的删除
- 监听数组项变化需要做特殊处理,无法通过下标来更改数组项,例如template中使用
{{arr[0].hobby}}
,而vm.arr[0] = {hobby: '喝水'}
这样的修改不会有效果,除非进一步$vm.$forceUpdate()
,或者使用vm.$set(this.arr, 0, {hobby: '喝水'})
;或者修改arr.length = 3
当把数组长度截取到3时,也监听不到修改。
2. ES6 Proxy
2.1 优点
- 拦截在对象层面,而非属性层面。所以不需要枚举属性值,也可以监听到属性的新增和属性的删除
- 深层对象可以在访问该属性值的时候再去设置代理,而非初始的时候就深度遍历
- 不更改原对象,new Proxy返回一个新的代理对象,之后我们只需要操作这个代理
- 可以直接拦截数组操作
- 有13种拦截方式 , 支持的其他拦截类型
new Proxy(terget, {
get() { }, // 拦截属性读取
set() { }, // 拦截属性值设置
deleteProperty() { }, // 拦截对属性的delete操作
});
2.2 代理对象
const p = new Proxy(pager, {
set(target, key, newValue) {
target[key] = newValue;
console.log('设置属性:', key, newValue);
},
deleteProperty(target, key) {
if (key in target) {
delete target[key];
return true;
}
console.log('监听到属性删除', arguments)
return false;
}
});
p.newAttr = 'wxm'; // 可以监听到新增属性
delete p.newAttr;
我们发现,拦截属性不需要枚举所有属性名去遍历,同时也能拦截到新增属性和delete操作。
2.3 代理数组
var arr = [1, 2, 3];
var proxyArr = new Proxy(arr, {
set(target, prop, value, proxy) {
target[prop] = value;
console.log('拦截到更改', arguments)
return true; // return 一个布尔值表示是否设置成功
}
});
// 以下情况都能被拦截到:
// 通过index来修改项
// 使用超出的index设置值以扩充数组长度
// 通过设置length来扩充或缩小数组长度,只会被拦截到1次(length属性的更改),将项设置为empty不会拦截
// 通过push新增项:会被拦截到2次,一次是index项的更改,一次是数组length属性的更改
// 通过unshift新增项:会被拦截到arr.length(因为每项都会被更改)+1(length属性更改)次,
// 通过reverse翻转顺序,会被拦截到arr.length次,因为每项都会被更改
// 通过pop删除元素,拦截到length更改
// 通过shift删除元素:拦截到第一项后面每项的更改和length的更改
// 通过splice删除和新增元素,会被拦截N次,项的更改和length的更改
// 通过sort排序,拦截到项的更改
- 不能直接修改proxyArr的指向
- 如果数组项是对象的话,对象属性变化也监听不到
2.4 代理函数 handler.apply
function add(a, b) {
return a + b + this.name;
}
var proxyAdd = new Proxy(add, {
// 拦截函数执行
apply(func, ctx, args) {
console.time("run");
let val = func.apply(ctx, args);
console.timeEnd("run");
return val;
}
});
// 跟普通函数调用方法相同,这些调用方式都能被拦截到
proxyAdd(1, 2);
proxyAdd.call({name: 'wxm'}, 1, 2);
proxyAdd.apply({name: 'wxm'}, [1,2]);
3. 使用Proxy做响应式
- get:在访问对象属性的时候收集依赖:在属性维度上收集函数,比如说在computed或template模板中访问了哪个对象的哪些属性
- set:当写入属性值的时候,如果存在这个属性的依赖函数,就去通知这些依赖,以做相应的更新
- 拦截对象的读和写
function reactive(data) {
return new Proxy(data, {
get(target, key, receiver) {
track(target, key); // 在访问对象属性的时候收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key); // 当写入属性值的时候通知依赖
return res;
}
});
}
- 收集依赖。将依赖收集到bucket里:key为原对象,value(Map类型)存各个属性的effect函数(key为属性名,value为Set类型)
let bucket = new WeakMap();
let curEffect;
// 收集依赖,收集的时候需要将依赖函数赋值给全局的curEffect
function track(target, key) {
if (!curEffect) {
return;
}
let depsMap = bucket.get(target); // 这个对象所有属性的依赖
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key); // 当前属性的所有依赖
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(curEffect); // 在set里加入当前effect函数
}
- 通知依赖
// 通知依赖
function trigger(target, key) {
let depsMap = bucket.get(target); // 这个对象所有属性的依赖
if (!depsMap) {
return;
}
let deps = depsMap.get(key); // 当前属性的所有依赖
if (!deps) {
return;
}
// 通知deps里的所有依赖函数
deps.forEach(effect => {
effect();
});
}
- 假设我们需要在修改title的时候执行effectTitle,修改name的时候执行effectName
let data = reactive({ title: '详情', name: '姓名' });
function effectTitle() { // 依赖data.title的函数
console.log(data.title + ':title附加操作')
}
function effectName() { // 依赖data.name的函数
console.log(data.name + ':name附加操作')
}
- 我们还没有触发属性的get,所以还没有收集依赖。接下来触发get,并在合适的时机给curEffect赋值 ,以把effectTitle和effectName加到bucket中去
function effect(fn) {
const effectFn = () => {
curEffect = effectFn; // 给curEffect赋值
fn();
};
effectFn();
}
effect(effectTitle); // effect内部会立即执行effectTitle,而effectTitle内部读取了title,会触发收集依赖
effect(effectName);
-
执行依赖函数,依赖函数内部读取属性值会触发get,get里面会收集依赖。但是依赖函数会被执行多次,而只需要收集一次。但是目前track里面,我们并没有手动判断是否重复收集了,而是每次都执行了deps.add(curEffect);但是由于Set的特性,同一个依赖函数并不会被重复添加,这也是使用了Set而非Array来存储的原因。
-
此时还不能满足深层对象的监控,例如,在data.info.qty改变时同时renderQty
let data = reactive({ info: { qty: 10 } });
function renderQty() {
console.log(data.info.qty + '元');
}
effect(renderQty);
data.info = {qty: 30}; // 这种情况会触发renderQty
data.info.qty = 20; // 这种情况不会触发renderQty
所以依赖在访问属性值的时候,如果发现值isObject,就返回经过reactive处理的代理对象:
const isObject = (val) => val !== null && typeof val === 'object';
const proxyMap = new Map();
function reactive(data) {
if (!isObject(data)) {
console.warn(`value cannot be made reactive: ${String(target)}`);
return data;
}
const existingProxy = proxyMap.get(data);
if (existingProxy) {
return existingProxy;
}
const p = new Proxy(data, {
get(target, key, receiver) {
track(target, key); // 在访问对象属性的时候收集依赖
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res; // 如果值是对象则返回代理对象
},
set(target, key, value, receiver) {
let res = Reflect.set(target, key, value, receiver);
trigger(target, key); // 当写入属性值的时候通知依赖
return res;
}
});
proxyMap.set(data, p);
return p;
}
再次测试:
data.info = {qty: 30}; // 会触发renderQty
data.info.qty = 20; // 会触发renderQty
4. Reflect
const p = new Proxy(pager, {
get(target, key) {
return target[key]; // 对比下面的使用Reflect.get
},
});
const p1 = new Proxy(pager, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
});
-
使用
Reflect.get
获取属性值:这里第三个参数receiver === p1,是为了传递正确的调用者指向 -
使用
Reflect.set
修改属性:如果用=
直接赋值,无法判断是否修改成功了,而Reflect.set
可以通过返回值来判断 -
使用
Reflect.defineProperty
定义属性: 如果使用Object.defineProperty重复定义一个属性,js会报错,但是使用Reflect.defineProperty并不会报错,而是可以通过返回值来判断是否操作成功。所以在框架中选择使用Reflect让程序更健壮.