基本实现
背景:因为在目前的项目种大量使用了Proxy Api 但是产品突然提出ie浏览器无法打开项目 所以被迫营业....
目前实现的是Proxy set,get,construct拦截, 核心原理还是耳熟能详的 Object.defineProperty , 废话不多说上代码
const ISALIVE = Symbol('isAlive');
function Proxy(pasproxy = {}, handler = {}) {
//类型错误处理
if (!(pasproxy instanceof Object) || !(handler?.constructor === Object)) {
throw new Error(`Cannot create proxy with a non-object as target or handler`)
}
//@ 首先定义默认的代理方法,在Proxy中handler不传会报错,但是为了方便我给他一个默认值,默认取代理对象的属性
const _handler = {
get(obj, key) {
return obj[key];
},
set(obj, key, value, proxy) {
// console.log('proxy',obj,key,value,)
return (obj[key] = value);
},
};
Object.assign(_handler, handler);
const targetIsArray = pasproxy instanceof Array
//@ mirror 是一个镜像属性,作为被代理对象与代理关系之间的中间层, 如果直接在被代理对象上调用defineProperty会污染数据源,
const mirror = pasproxy && targetIsArray ? [] : {};
Object.keys(pasproxy).some((k, index) => {
//给当前属性特殊标识
mirror[k] = ISALIVE;
Object.defineProperty(mirror, k, {
get() {
return _handler.get(pasproxy, k);
},
set(value) {
return _handler.set(pasproxy, k, value, pasproxy);
},
});
});
mirror['[[Target]]'] = pasproxy
Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
return mirror
}
const p = new Proxy({name:'juejin'},{get(obj,key){return obj[key] + '!'}})
p.name // juejin !
以上就是最基本的实现了, mirror作为中间层, 当set或者get方法响应,返回的总是被代理对象的key,所以原型上的属性也能被转发到.
其中 mirror[k] = ISALIVE 是一个特殊标识, 告诉系统当前属性存在并且被注册过了,原因的话等下说.
因为 mirror作为一个中间层其属性只是作为与代理对象通信的桥梁,不能准确的反应数据,
所以设置 mirror['[[Target]]'] 用于查看当前被代理对象的属性值,便于调试
现在这个版本最大的问题就在于:
1.无法动态拦截属性(比如我new的时候代理对象是{a:1},后面我添加了b=2属性,这个时候就没效果了)
2.无法拦截诸如Date,Array,实例化对象的方法(就比如:array, slice, date.now,根本原因是这些属性不能被直接迭代 )
原生Proxy的痛点
对于第1点动态拦截属性目前除了脏检查没有特别好的办法,而且没办法保证一致性,但是这一点也没必要太纠结,原生Proxy的拦截方法也有累赘的地方 比如:
你看你只想拦截slice,但是length,constructor这种你不需要响应的属性也响应了.这样程序就有了额外的负担.
其实我们一般再使用Proxy的时候对于要拦截的对象或属性我们应该是提前预知的,因为你只有知道你要拦截什么才能对其做相应的操作, 所以其实对于属性拦截这一点我们可以做到提前声明或者后置注册. 但是提前声明会显得数据结构累赘不太优雅,而且对于数组上得键值操作相当不友
好,所以我们来实现后置注册:
通过implant方法解决动态属性的问题
为Proxy实列添加一个implant方法:
const ISALIVE = Symbol('isAlive');
function Proxy(pasproxy = {}, handler = {}) {
if (!(pasproxy instanceof Object) || !(handler?.constructor === Object)) {
throw new Error(`Cannot create proxy with a non-object as target or handler`)
}
const _handler = {
get(obj, key) {
return obj[key];
},
set(obj, key, value, proxy) {
// console.log('proxy',obj,key,value,)
return (obj[key] = value);
},
construct(target, args) {
return new target(...args);
},
max: 100, // 防止数组过大
};
Object.assign(_handler, handler);
const implant = (k, initValue) => {
//防止添加undefinde 这种无效字段
if (Type.isEmpty(k)) return;
//不重复注册已经注册过的属性
if (Key.get(mirror, k) === ISALIVE) return;
//防止属性被删除时 后期不能注册
if (mirror[k] === undefined && Key.get(mirror, k) === ISALIVE) {
Key.del(mirror, k)
}
mirror[k] === undefined ? mirror[k] = ISALIVE : null;
pasproxy[k] === undefined ? pasproxy[k] = initValue : null;
//防重标识
Key.set(mirror, k, ISALIVE)
Object.defineProperty(mirror, k, {
get() {
return _handler.get(pasproxy, k);
},
set(value) {
return _handler.set(pasproxy, k, value, pasproxy);
},
});
};
const targetIsArray = pasproxy instanceof Array
const mirror = pasproxy && targetIsArray ? [] : {};
mirror.implant = implant;
//让该属性不能被遍历防止迭代时产生意外的结果
Object.defineProperty(mirror, 'implant', { enumerable: false })
//
Object.keys(pasproxy).some((k, index) => {
mirror[k] = ISALIVE;
Object.defineProperty(mirror, k, {
get() {
return _handler.get(pasproxy, k);
},
set(value) {
return _handler.set(pasproxy, k, value, pasproxy);
},
});
//考虑到数组数据过大情况防止不必要的监听
if (targetIsArray && index >= _handler.max) return true
});
mirror['[[Target]]'] = pasproxy
Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
return mirror
}
const p = new Proxy({name:'juejin'},{get(obj,key){return obj[key] + '!'}})
p.implant('loc','中国')
p.loc // 中国 !
先来说下 ISALIVE 的作用 , 本质上就是一个占位符用来判断当前属性是否被注册并且已经存在 , 因为对象可能面临 delete obj[key] 这
样的操作, 这样信息沟通的桥梁也就断了, 但是属性又面临重新被监听的可能, 比如再可编辑表格中, 再提交数据时 需要删除不必要的键位,
然后数据回传后以前删除键位的属性还是需要被重新监听.
实列方法的拦截
然后就是 _handler.max 这个属性 虽然有诸如操作: arr[N] 的必要 但是大部分情况下 不是所有的键位都要实时监听, 如果出现1k 1W这样的监控需求可以手动改变max的值达到目的
然后到这里动态属性的问题也算是马马虎虎的解决了, 那么实列化对象上的方法又如何去处理呢? 比如拦截数组或者日期上的方法,又或者拦截
class A上的log方法
class A{
constructor(){}
log(){}
}
现在比较主流的做法是直接去代理一个方法,比如
arr.slice=(...arg)=>{
Arrar.prototype.slice.call ....
....
return ...
}
现在我们可以直接 implant!
const p = new Proxy([],{get(obj,key){
if(key==='slice'){
return ()=>[1,2,3]
}
return obj[key]
}})
p.implant('slice')
p.slice() // [1,2,3]
代码简洁了很多!
但是如果每次new Proxy 都要注册一次slice也显得非常冗余,怎么办呢?
我想到的办法是为数据添加字段表,比如为Array这种常用的数据类型为它添加一套固有的属性字段, 然后再配合一个类型检查函数再每次代理数组类型时自动迭代执行implant添加注册表中的属性
定义注册表与Proxy类型检查函数
//注册表
const typeGroup = [
//提前预置数组上的所有方法
{
type: Array,
fields: `concat,constructor,copyWithin,entries,every,fill,filter,find,findIndex,flat,flatMap,forEach,includes,indexOf,join,keys,lastIndexOf,map,pop,push,reduce,reduceRight,reverse,shift,slice,some,sort,splice,toLocaleString,toString,unshift,values`.split(',')
},
]
typeGroup.set = (val) => {
if (!val.constructor === Object) throw new Error('新增注册表类型错误')
typeGroup.some(member => {
//相同类型合并
if (member.type === val.type) {
member.fields = [...member.fields, ...val.fields]
return true
}
}) || typeGroup.push(val)
}
//typecheck
function ProxyTypeCheck(target, handler) {
const proxy = Proxy(target, handler)
typeGroup.some(({ type, fields }) => {
if (target instanceof type) {
fields.forEach(k => proxy.implant(k))
return true
}
})
return proxy
}
ProxyTypeCheck.type = (type, arr) => {
const _arr = Array.isArray(arr) ? arr : [arr]
typeGroup.set({ type, fields: _arr })
}
module.exports = ProxyTypeCheck;
导入进来试试
import Proxy form 'Proxy'
Proxy.type(A,'log')
Proxy.type(Date,['now','parse'])
const p = new Proxy([],{get(obj,key){
if(key==='slice'){
return ()=>[1,2,3]
}
return obj[key]
}})
p.slice() // [1,2,3]
const p2= new Proxy(new A(),{get(obj,key){
if(key==='log'){
return (str)=>{ console.log(str)}
}
return obj[key]
}})
p2.log('hello word') // helloword
好了看起来已经结束了,但是在原生 Proxy Api 中是可以拦截被new操作符执行的构造函数的
拦截construct构造函数
function monster1(disposition) {
this.disposition = disposition;
}
const handler1 = {
construct(target, args) {
console.log('monster1 constructor called');
// expected output: "monster1 constructor called"
return new target(...args);
}
};
const proxy1 = new Proxy(monster1, handler1);
console.log(new proxy1('fierce').disposition);
// expected output: "fierce"
感觉有点吐血, 我们再试试看看这个鬼东西能不能拦截下来
function Proxy(pasproxy = {}, handler = {}) {
if (!(pasproxy instanceof Object) || !(handler.constructor === Object)) {
throw new Error(`Cannot create proxy with a non-object as target or handler`)
}
const _handler = {
get(obj, key) {
return obj[key];
},
set(obj, key, value, proxy) {
// console.log('proxy',obj,key,value,)
return (obj[key] = value);
},
construct(target, args) {
return new target(...args);
},
max: 100,
};
Object.assign(_handler, handler);
const implant = (k, initValue) => {
if (Type.isEmpty(k)) return;
if (Key.get(mirror, k) === ISALIVE) return;
if (mirror[k] === undefined && Key.get(mirror, k) === ISALIVE) {
Key.del(mirror, k)
}
mirror[k] === undefined ? mirror[k] = ISALIVE : null;
pasproxy[k] === undefined ? pasproxy[k] = initValue : null;
Key.set(mirror, k, ISALIVE)
// Object.defineProperty(mirror, k, { enumerable: false })
Object.defineProperty(mirror, k, {
get() {
return _handler.get(pasproxy, k);
},
set(value) {
return _handler.set(pasproxy, k, value, pasproxy);
},
});
};
//拦截new操作符
const ProxyTarget = function (...arg) {
return _handler.construct(pasproxy, arg)
}
const targetIsFunction = typeof pasproxy === 'function'
const targetIsArray = pasproxy instanceof Array
const mirror = pasproxy && targetIsArray ? [] : (targetIsFunction ? ProxyTarget : {});
mirror.implant = implant;
Object.defineProperty(mirror, 'implant', { enumerable: false })
//
Object.keys(pasproxy).some((k, index) => {
mirror[k] = ISALIVE;
Object.defineProperty(mirror, k, {
get() {
return _handler.get(pasproxy, k);
},
set(value) {
return _handler.set(pasproxy, k, value, pasproxy);
},
});
if (targetIsArray && index >= _handler.max) return true
});
mirror['[[Target]]'] = pasproxy
Object.defineProperty(mirror, '[[Target]]', { enumerable: false })
return mirror
}
改动不大 ,接下来试试
class A {
constructor() {
}
log() {
console.log('日志')
}
}
const p = new Proxy(A, {
construct(target, args) {
console.log('monster1 constructor called');
return new target(...args);
}
})
new p().log()
好了! 大功告成
悬而未决的问题
关于 Proxy 虽然平常用的最多的是get,set这两个api,但是仔细翻看Proxy文档还是有很多东西是技巧性弥补不了的
比如:
handler.setPrototypeOf() 原型链操作拦截
handler.deleteProperty() delete 操作符拦截
handler.has() in 操作符拦截
虽然不能做到 100% 实现Proxy , 但是通过对两个基础api的封装笔者在实际使用过程中也并没有大的问题, 当然大佬们有更好的方法多多讨论,多多指教.
最后总结
为了兼容Proxy所做的努力
1.通过封装 Object.defineProperty伪造Proxy Api
2.通过implant+属性注册表解决动态属性的问题
3.通过类型检查函数减少implant冗余性
4.通过max属性解决超大数组的痛点