ps:观察者模式 (下面有重点)
Vue作为前端框架的三驾马车之一,在众多前端项目中具有极其重要的作用。
Vue中具有一个重要的功能点——“数据绑定”。使用者无需关心数据是如何绑定到dom上面,只需要关注数据本身即可。
那实现其功能的原理是什么?
阅读官方文档(v2.0),我们会发现:
把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。
关键字是Object.defineProperty,在MDN文档找到说明如下:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
我们再仔细查询在MDN文档的说明会发现,Object.defineProperty()存在两种属性描述符:
数据描述符(简略介绍)
存取描述符
至此也就引出了getter/setter。
让我们通过一个例子来测试一下。
首先,建立一个英雄(Hero)对象并赋予其一些属性:
let hero = {
name:'赵云',
hp: 100,
sp: 100
}
然后使用Object.defineProperty()来对其具有的一些属性进行修改,并且在控制台输出修改的内容:
Object.defineProperty(hero, 'hp', {
set (val) {
console.log(`Set hp to ${val}`);
return val;
}
})
hero.hp = '200';
// --> Set hp to 200
假若把console.log('Set hp to ${val}') 改为 element.innerHTML = val,是不是就可以实现数据绑定了?
那让我们再修改一下英雄的属性,假设英雄拥有很多装备:
let hero = {
name:'赵云',
hp: 100,
sp: 100,
equipment:['马','长枪']
}
我们把“佩剑”添加到英雄的装备中,并且输出在控制台:
Object.defineProperty(hero.equipment, 'push', {
value () {
this[this.length] = arguments[0];
}
})
hero.equipment.push('佩剑');
console.log(hero.equipment);
// --> [ '马','长枪', '佩剑' ]
由此,我们可以看到对象的属性变化可以依靠get()和set()方法去追踪和改变;但对于数组则需要使用value()方法实现。
let data = {
list: []
}
Object.keys(data).forEach(function (key) {
let value = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
console.log(`Setting`);
value = newValue;
return true;
}
})
})
data.list.push(1); // 监听不了push 不会打印Setting
// data.list = [1, 2, 3]; //直接赋值才会打印Setting
console.log(data.list);
显然,这不是最好的方法,那有没有更好的方法可以简化对象或数组属性变化呢?
Object.defineProperty 方式重写数组
//Array.prototype 这个表示数组本身
//虽然这种监听能解决上面的 数组监听缺陷,但是只能用value() 方式,不能使用get\set
let arrayMethod = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
//便利每个method做监听它们的属性
Object.defineProperty(arrayMethod, method, {
enumerable: true,
configurable: true,
value: function () {
let args = [...arguments]
Array.prototype[method].apply(this, args);
console.log(`operation: ${method}`)
dep.notify();
}
})
});
[Array Object].__proto__ = arrayMethod; //最后挂载到原型上
答案是肯定的。
Proxy意思为“代理”,即在访问对象之前建立一道“拦截”,任何访问该对象的操作之前都会通过这道“拦截”,即执行Proxy里面定义的方法。
let pro = new Proxy(target,handler);
get(target,prop,receiver) —— 拦截对象属性的读取
set(target, propKey, value, receiver) —— 拦截对象的设置
has(target, propKey) —— 拦截 propKey in proxy 的操作
deleteProperty(target, propKey) —— 拦截 delete proxy[propKey] 的操作
ownKeys(target) —— 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环
getOwnPropertyDescriptor(target, propKey) —— 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc) —— 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs)
preventExtensions(target) —— 拦截Object.preventExtensions(proxy)
getPrototypeOf(target) —— 拦截 Object.getPrototypeOf(proxy)
isExtensible(target) —— 拦截 Object.isExtensible(proxy)
setPrototypeOf(target, proto) —— 拦截Object.setPrototypeOf(proxy, proto)
apply(target, object, args) —— 拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
construct(target, args) —— 拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)
Proxy支持13种拦截行为(handle),针对解决上一节的问题,简单介绍下其中2种拦截行为,get与set。
get(target, propKey, receiver)
用于拦截某个属性的读取操作,可以接受三个参数:
set(target, propKey, value, receiver)
用于拦截某个属性的赋值操作,可以接受四个参数:
在解决上一节问题之前,先一同看几个实例。
let hero = {
name: "赵云",
age: 25
}
let handler = {}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> 赵云
heroProxy.name = "黄忠";
console.log(heroProxy.name);
// --> 黄忠
解析:
创建hero对象为所要拦截的对象;
拦截操作对象handler为空,未对拦截对象设定拦截方法;
该情况下heroProxy直接指向原对象target,访问heroProxy等同于访问target,所以结果为target中的结果。
let hero = {
name: "赵云",
age: 25
}
let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
heroProxy.name = '黄忠';
console.log(heroProxy.name);
// --> 英雄名是赵云
// --> 赵云 change to 黄忠
// --> 英雄名是黄忠
解析:
创建hero对象为所要拦截的对象;
handler对象为拦截对象后执行的操作,这里get方法为读取操作,即用户想要读取heroProxy中的属性时执行的拦截操作。
最后创建一个Proxy实例,当读取heroProxy中的属性时,结果打印出来的总是“黄忠”字符串。
Proxy也可以作为其他对象的原型对象使用。
let hero = {
name: "赵云",
age: 25
}
let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}
let heroProxy = new Proxy(hero, handler);
let obj = Object.create(heroProxy);
console.log(obj.name);
obj.name = '黄忠';
console.log(obj.name);
// --> 英雄名是赵云
// --> 赵云 change to 黄忠
// --> 英雄名是黄忠
解析:
在实例2的基础上,将heroProxy作为obj的原型对象使用。
虽然obj本身没有name这个属性,但是根据原型链,会在heroProxy上读取到name属性,之后会执行相对应的拦截操作。
在我们对Proxy有了一定了解后,可以尝试解决上一节的问题。
首先,还是定义一个英雄:
let hero = {
name:'赵云',
hp: 100,
sp: 100,
equipment:['马','长枪']
}
接着,定义一个handler:
let handler = {
set(target, property, value) {
console.log(`hero's ${property} change to ${value}`);
target[property] = value;
return true;
}
}
然后,修改英雄的hp值
let heroProxy = new Proxy(hero, handler);
heroProxy.hp = 200;
// --> hero's hp change to 200
console.log(hero.hp);
// --> 200
最后,同样把“佩剑”添加到英雄的装备中
let heroProxy = new Proxy(hero.equipment, handler);
heroProxy.push('佩剑');
// --> hero's 2 change to 佩剑
// --> hero's length change to 3
console.log(hero.equipment);
// --> ["马", "长枪", "佩剑"]
可以发现,heroProxy.push('佩剑');触发了两次set,原因是push即修改了hero.equipment的内容,又修改了hero.equipment的length。
在了解了Proxy之后,细心的我们一定发现,若需要在Proxy内部调用对象的默认行为,该如何实现?
Reflect正是ES6 为了操作对象而提供的新 API。
Reflect对象一共有 13 个静态方法(匹配Proxy的13种拦截行为)。
大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。
下面通过3个实例来对比Object对象方法与Reflect对象方法。
//Object对象方法
try {
Object.defineProperty(target, name, property);
} catch (e) {
console.log("error");
}
//Reflect对象方法
if (Reflect(target, name, property)) {
console.log("success");
} else {
console.log("error")
}
解析:
由于Reflect(target, name, property)返回的是boolean,代码语义性更好。
let hero = {
name: '赵云',
hp: 100,
sp: 100,
equipment: ['马', '长枪']
}
//Object对象方法
console.log('name' in hero);
// --> true
//Reflect对象方法
console.log(Reflect.has(hero,'name'));
// --> true
解析:
Object操作是命令式,而Reflect让它们变成了函数行为
let hero = {
name: '赵云',
hp: 100,
sp: 100,
equipment: ['马', '长枪']
}
let handler = {
get(target, name, receiver) {
if (name === "name") {
console.log("success");
} else {
console.log("failure");
}
return Reflect.get(target, name, receiver);
}
}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> success
// --> 赵云
解析:
Reflect对象的操作和Proxy对象的操作一一对应,在Proxy的拦截操作中,可以直接利用Reflect对象直接获取Proxy的默认值。
掌握了Proxy与Reflect的知识点后,除了解决文章开头的数据绑定问题之外,挑选日常编码中容易遇见的两种情况进行编码实践。
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦数据有变化,函数就会自动执行。
let hero = {
name: '赵云',
hp: 100,
sp: 100,
equipment: ['马', '长枪']
}
const handler = {
set(target, key, value, receiver) {
//内部调用对应的 Reflect 方法
const result = Reflect.set(target, key, value, receiver);
//执行观察者队列
observableArray.forEach(item => item());
return result;
}
}
//初始化Proxy对象,设置拦截操作
const createProxy = (obj) => new Proxy(obj, handler);
//初始化观察者队列
const observableArray = new Set();
const heroProxy = createProxy(hero);
//将监听函数加入队列
observableArray.add(() => {
console.log(heroProxy.name);
});
heroProxy.name = "黄忠";
// --> 黄忠
该实例在set拦截行为中加入了监听函数的执行,使每一次值的改变均能被监听。
实现对象间的单继承,比如obj2继承obj1,可以使用Object.setPrototypeOf方法,但是没法实现多继承。
const people = {
name: 'people',
run() {
console.log('people.run:', this.name);
}
};
const powerMan = {
name: 'powerMan',
run() {
console.log('powerMan.run:', this.name);
},
fight() {
console.log('powerMan.fight:', this.name);
}
};
const handler = {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
return Reflect.get(target, name, receiver);
}
else {
for (let P of target[Symbol.for('[[Prototype]]')]) {
if (Reflect.has(P, name)) {
return Reflect.get(P, name, receiver);
}
}
}
}
};
const hero = new Proxy({
name: 'hero',
strike() {
this.run();
this.fight();
}
}, handler);
hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();
// --> people.run:hero
// --> powerMan.fight:hero
用了一个自定义的属性Symbol.for("[[Prototype]]")来表示要继承的多个父对象。
然后用Proxy来拦截所有hero中的get请求,使用Reflect.has方法检查hero中是否存在相应的属性或者方法。
如果存在,则直接转发;如果不存在,则遍历父对象列表,在父对象中逐个检查是否存在相应的属性或者方法。
若存在则调用。若不存在,则相当于get返回undefined。
{
//数据校验主程序
function validator(target,validator){
return new Proxy(target,{
_validator: validator,
set(target,key,value,proxy){
//判断当前对象是否有这个key值
if(target.hasOwnProperty(key)){
//执行校验条件函数
let va = this._validator[key];
//是否符合条件
if(!!va(value)){
return Reflect.set(target,key,value,proxy)
}
else{
throw Error(`不能设置${key}为${value}`)
}
}
else{
throw Error(`${key} 不存在`)
}
}
})
}
//校验条件函数
const personValidators = {
name(val){
return typeof val === 'string'
},
age(val){
return typeof val === 'number' && val > 18
}
}
//类
class Person{
constructor(name,age){
this.name = name;
this.age = age;
return validator(this,personValidators)
}
}
const person = new Person('xiaoming',30);
console.log(person); // {name: 'xiaoming' , age: 30}
person.name=38; // 设置失败,因为name需要是一个字符串
person.age=38; // 设置成功
}