[es6]proxy的原理和实现


代理模式

HeadFirst设计模式中,对代理模式的介绍如下。其主要的思想是将访问数据对象这一过程解耦。

[es6]proxy的原理和实现
img
[es6]proxy的原理和实现
img

使用方式

Proxy 是 ES6 中新增的功能,可以用来自定义对象中的操作,设计思想主要是基于设计模式中的代理模式,其原理图大致如下。

[es6]proxy的原理和实现
img

每次获取proxy,这个proxy就会返回对data的深拷贝,而要对data中的属性进行增删查等操作,也是直接对proxy下手就行。通过对整个对象的代理,就避免了访问之前要明确data中有什么属性的这一个过程。这样就实现了一个解耦合的过程,避免了直接操作data对象。基本的使用示例如下。

let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作

let person01 = {
    name"",
    testObj: {
        key1"1",
        key2"2",
        key3"3",
    }
};

// 设置了一个person01的代理
let personProxy01 = new Proxy(person01, {
    get(target,key){
        // console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
        return target[key];
    },
    set(target,key,value){
        // console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
        target[key]=value;
    }
})

proxy中的handler主要存放的是一些代理钩子函数,通过这些函数我们可以自定义一些数据劫持的方式,以此可以实现一个更优秀的订阅发布模式。

在对象的操作方法里面,建议使用ES6新增的Reflect,而Reflect里定义了对象操作的方法,利用Reflect可以方便地对被代理对象的操作。

handler中主要定义的函数如下所示(es2015中的源码)。

[es6]proxy的原理和实现
img
代理陷阱 覆写的特性 默认特性
get 读写一个属性值 Reflect.get()
set 写入一个属性 Reflect.set()
has in操作 Reflect.has()
deleteProperty delete操作符 Reflect.deleteProperty()
getAPrototypeof Object.getAPrototypeof () Reflect.getAPrototypeof ()
setAPrototypeof Object.setAPrototypeof () Reflect.setAPrototypeof ()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineaProperty Object.defineaProperty() Reflect.defineaProperty()
ownKeys Object.keys() 、 Object.getOwnPropertyNames()和 Object.getOwnPropertySysmbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 用new调用一个函数 Reflect.construct()

其中,最常使用的就是set和get了。

set(trapTarget,key,value,receiver) 这个set陷阱默认接收 四个参数

  • trapTarget 用于接收属性(代理的目标)的对象
  • key 要写入的属性键(字符串或Symbol类型)
  • value 被写入的属性的值
  • receiver 操作发生的对象(通常是代理)

get(trapTarget,key,receiver) 默认接受三个参数

  • trapTarget 被读取属性源对象(代理的目标)
  • key 要读取的属性键(字符串或Symbol类型)
  • receiver 操作发生的对象(通常是代理)

自己实现一个Proxy

思路

  • 参数为(target, handle),接受target目标数据对象,handle则是写代理陷阱函数

  • 定义一个深拷贝函数,实现对target的深拷贝(这里的深拷贝要支持对象、函数、数组等)

  • 获取target的深拷贝targetCopy

  • 遍历targetCopy.keys(),利用Object.defineProperty来为每个key覆写函数,并放入handler中对应函数

  • 设置一个拦截器,对后来添加入targetCopy的属性,为这个key添加覆写函数,并放入handler中对应函数(暂时无思路)

  • 返回这个targetCopy

按照思路写出来,大致如下。

function MyProxy(target, handle{
    this.clone = function (myObj{
        // 是否有嵌套结构
        if (typeof (myObj) !== 'object' || myObj == null) {
            return myObj;
        }

        var newObj = new Object();

        // 递归
        for (let i in myObj) {
            newObj[i] = this.clone(myObj[i]);
        }

        return newObj;
    }

    //深度克隆目标对象
    let targetCopy = this.clone(target);

    // 遍历目标克隆对象的所有属性  
    // 这里有一个问题,就是它只能操作已有的属性
    // 如果是后来往proxy添加的属性呢?怎么从proxy传给data本身?
    Object.keys(targetCopy).forEach(function (key{
        Object.defineProperty(targetCopy, key, {
            getfunction ({
                // 
                return handle.get && handle.get(target, key)
            },
            setfunction (newVal{
                if (!handle.set) {
                    handle.set(target, key, newVal);
                } else {
                    // 对象有这个属性时才允许被操作
                    handle.set && handle.set(target, key, newVal);
                }


            }
        });
    });

    // 返回
    return targetCopy;
}

下面来实践一下效果如何?


// ---------------------------------定义data--------------------------
let person01 = {
    name"",
    testObj: {
        key1"1",
        key2"2",
        key3"3",
    },
    list: [1,2,3]
};
let person02 = {
    name"",
    testObj: {
        key1"1",
        key2"2",
        key3"3",
    },
    list: [1,2,3]
};

// ----------------------------------设置proxy-------------------------
let personProxy01 = new Proxy(person01, {
    get(target, key) {
        // console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
        return target[key];
    },
    set(target, key, value) {
        // console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
        target[key] = value;
        // return Reflect.set(target, key, value);
    }
})

let personProxy02 = new MyProxy(person02, {
    get(target, key) {
        // console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
        return target[key];
    },
    set(target, key, value) {
        // console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
        target[key] = value;
    }
})
// ---------------------------------------------------------------------------



console.log("-------------------------es6自带的proxy---------------------");

personProxy01.name = 'hzs';  // set方法被拦截。。。
personProxy01.testObj = {
    key1"11",
    key2"22",
    key3"33",
}
personProxy01.list[0] = 5

console.log(personProxy01.name);   // get方法被拦截。。。
console.log(personProxy01.testObj);
console.log(personProxy01.testObj.key1);
console.log(personProxy01.list);
console.log("--------------------------------------");
console.log(person01.name);   // get方法被拦截。。。
console.log(person01.testObj);
console.log(person01.testObj.key1);
console.log(person01.list);

console.log("-----------------自己尝试实现的proxy---------------------");

personProxy02.name = 'hzs';  // set方法被拦截。。。
personProxy02.testObj = {
    key1"11",
    key2"22",
    key3"33",
}
personProxy02.list[0] = 5
console.log(personProxy02.name);   // get方法被拦截。。。
console.log(personProxy02.testObj);
console.log(personProxy02.testObj.key1);
console.log(personProxy02.list);

console.log("--------------------------------");
console.log(person02.name)
console.log(person02.testObj)
console.log(person02.testObj.key1);
console.log(person02.list);




console.log("-----------尝试往data本身添加新属性,proxy能否监听到?----------------")
person01.sex = "M"
person02.sex = "W"
console.log(person01.sex)
console.log(person02.sex)


console.log("-----------尝试向proxy添加新属性,data本身能否获取到?----------------")
personProxy01.home = "黄土高坡"
personProxy02.home = "青青草原"
console.log(person01.home)
console.log(personProxy01.home)
console.log(person02.home)
console.log(personProxy02.home)


console.log("-----------尝试往data本身添加function,proxy能否监听到?----------------")
person01.isMan = function(sex){
    return sex === "M";
}
person02.isWomen = function(sex){
    return sex === "W"
}
console.log(person01.isMan("M"))
console.log(person02.isWomen("W"))  //  undefined


console.log("-----------尝试向proxy添加function,data本身能否获取到?----------------")
personProxy01.isWomen = function(sex){
    return sex === "W";
}
personProxy02.isMan = function(sex){
    return sex === "M"
}
console.log(person01.isWomen("W"))
// console.log(person02.isMan("M")) 会报错


运行结果如下图:

[es6]proxy的原理和实现
img
[es6]proxy的原理和实现
img

可以看到,前面基本上是没什么问题的,但是到后面,当给proxy添加data中没有的值时,自定义的proxy并不能将新添加的属性加入到data中,而es6自带的proxy则可以实现。我也不太知道该怎么改进,因为object.defineProperty并不提供对于对象本身的拦截。

不过,实际上我个人并不提倡通过对proxy向data中添加没有的数据,一般这种情况下还是直接操作dataObj本身就好了,这也比较符合常用的逻辑。proxy实现的主要目的还是帮助我们监听整个对象,在访问数据对象的时候实现解耦,不用过度关心dataObj中有什么属性。

Proxy使用示例:实现数据绑定

众所周知,VUE3之前,双向绑定是使用Object.defineProperty,而VUE3则是基于proxy来实现的。为什么呢?肯定是为了改进一些缺陷啊!

Object.defineProperty 不足

  • 无法监听数组的变化:数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse.,vue中能监听是因为对这些方法进行了重写
  • 只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历。对于原本不在对象中的属性难以监听。

实现

可以很方便的使用 Proxy 来实现一个数据绑定和监听

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let dataObj = { a1 }
let value
let p = onWatch(dataObj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
[es6]proxy的原理和实现
img

结语

对于代理模式、defineProperty还有使用proxy实现双向绑定这块,我会争取在写一些学习笔记来继续发出的。

参考资料

《js之proxy实现》      https://blog.csdn.net/qq_38603437/article/details/92855683

前端进阶之道》       https://yuchengkai.cn/docs/frontend/#proxy

《headfirst设计模式》


原文始发于微信公众号(豆子前端):[es6]proxy的原理和实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/56818.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!