likes
comments
collection
share

【磨破嘴皮】Proxy & Reflect

作者站长头像
站长
· 阅读数 71

Proxy 是在ES2015中添加的语法除去IE外,几乎所有浏览器的新版本都对Proxy兼容,他的原理是将proxy的所有内部方法转发到target对象上

【磨破嘴皮】Proxy & Reflect

Vue3也使用了Proxy代替了Object.defineProperty,实现响应式

基本使用

const target = {}
const targetProxy = new Proxy(target, handler)

利用Proxy代理获取和设置数据的操作

Js强制执行一些不变量,用于返回值

  • Set 值已经写入成功,必须需返回true, 反之返回false
  • Delete 删除成功,必须返回true, 反之返回false
const obj = {
  name: 'xieziqian',
  age: 20
};

const proxy = new Proxy(obj, {
  get(target, prop) {
    console.log(`正在读取 ${prop} 属性`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`正在设置 ${prop} 属性为 ${value}`);
    target[prop] = value;
    return true;
  },
  deleteProperty(target, prop) {
    console.log(`正在删除 ${prop}`);
    delet target[prop]
  }
});

console.log(proxy.name); // 输出 "正在读取 name 属性" 和 "xieziqian"
proxy.age = 21; // 输出 "正在设置 age 属性为 21"
console.log(proxy.age); // 输出 21

ownKeys拦截实现私有变量

const obj = {
    _age: 100,
    name: 'xiaoming'
}
const proxy = new Proxy(obj, {
    ownKeys(target) {
        // filter 过滤
        return Object.keys(target).filter(key => !key.startWidth('_'))
    }
})
// 
const proxy2 = new Proxy(obj, {
    ownKeys(target) {
        // filter 过滤
        return ['a', 'b', 'c']
    }
})
Object.keys(proxy2) // []

Object.keys仅返回带有enumerable标志的属性,为了检查它,方法会对每个属性调用内部方法[[GetOwnProperty]]来获取他的描述符,在这个例子中,没有获取到,所以abc都会被略过。所以这里proxy2返回空

可以使用getOwnPropertyDescriptor来拦截Object.keys()获取属性描述符的动作。并返回enumerable为true此时Object.keys()就会返回abc了

const proxy2 = new Proxy(obj, {
    ownKeys(target) {
        // filter 过滤
        return ['a', 'b', 'c']
    },
    getOwnPropertyDescriptor(target, prop) {
        return {
            enumerable: true,
            configurable: true
        }
    }
})
Object.keys(proxy2) // abc

下面是js属性的描述符

  • value:属性的值。
  • writable:一个布尔值,指示属性是否可以被重新赋值。
  • enumerable:一个布尔值,指示属性是否可以被枚举(使用for...in或Object.keys()等方法)。
  • configurable:一个布尔值,指示属性是否可以被删除或修改描述符。
  • get:一个函数,返回属性的值。
  • set:一个函数,设置属性的值。

为什么不能在Proxy中返回target对象本身

会造成原始对象泄漏,不可修改属性被修改。可以看下面这段代码

const obj = {
    _name: 'xiaoming'
    func() {
        console.log(this._name)
    }
}

const pObj = new Proxy({
    get(target, prop) {
        if(prop.startWith('_')) {
            throw new Error(prop+'是私有属性')
        }
        if(typeof target[prop] === 'funciton') {
            return target[prop].bind(target)
        }
        return target[prop]
    },
    set(target, prop, val) {
        if(props.startWith('_')) {
            throw new Error(prop+'是私有属性')
            return false
        }
        target[prop] = val
        return true
    }
})

当对象中的函数被代理对象调用时,函数中的this其实是代理后的对象,这时,对于this的操作,也会被拦截,在这个例子中对于_name属性的访问就会被拦截,所以我们需要使用bind将this修改为原始对象,才能获取到_name属性,此时func被修改的话,就很容易能将原始对象传递给外界

let targetObj = null
pObj.func = function () {
    targetObj=this
}
pObj.func()
// 利用泄漏的原始对象,直接获取_name私有属性
console.log(targetObj._name) // 小明

利用Proxy转发函数自身的属性

使用高阶函数,实现一个延迟执行的方法

function delay(fn, time) {
    return function () {
        setTimeout(()=>{
            fn.apply(this, arguments)
        }, time)
    }
}
function print6 () {
    console.log('666')
}

const delayPrint6 = delay(print6, 1000) // 666
delayPrint6.length // 0
delayPrint6.name // ''

返回一个新函数,失去了对原函数属性length 和name的访问,使用proxy实现

function delay(fn, time) {
    return new Proxy(fn, {
        apply(target, thisArg, args) {
            setTimeout(()=>{
               // target.apply(this, arguments)
               target.apply(thisArg, args)
            }, time)
        }
    })
}
function print6 (str = '666') {
    console.log(str)
}

const delayPrint6 = delay(print6, 1000) // 666
delayPrint6.length // 0
delayPrint6.name // 'print6'

以下是Proxy现阶段支持的 trap,以及他对应的内部方法

Handler 名称参数描述/触发条件
get(target, property, receiver)[[Get]]target: 目标对象 property: 要获取的属性 receiver: 操作发生的对象拦截对象属性的读取操作
set(target, property, value, receiver)[[Set]]target: 目标对象 property: 要设置的属性 value: 要设置的值 receiver: 操作发生的对象拦截对象属性的赋值操作obj.attr = '' 时触发
has(target, property)[[GetOwnProperty]]target: 目标对象 property: 要判断是否存在的属性拦截 in运算符的操作
deleteProperty(target, property)[[Delete]]target: 目标对象 property: 要删除的属性拦截对象属性的删除操作删除操作delete
apply(target, thisArg, argumentsList)[[Call]]target: 目标函数 thisArg: 函数调用时的 this对象 argumentsList: 函数调用时的参数列表拦截函数的调用操作函数执行 .call() .apply() func() 都会触发
construct(target, argumentsList, newTarget)[[Construct]]target: 目标构造函数 argumentsList: 构造函数调用时的参数列表 newTarget: 最初被调用的构造函数拦截对象的构造函数操作被new调用时触发
getPrototypeOf(target)[[GetPrototypeOf]]target: 目标对象获取对象原型时拦截的操作Object.getPrototypeOf
setPrototypeOf(target, proto)[[SetPrototypeOf]]target: 目标对象 proto: 要设置的原型对象设置对象原型时拦截的操作Object.setPrototypeOf
isExtensible(target)[[IsExtensible]]target: 目标对象判断对象是否可扩展时拦截的操作Object.isExtensible
preventExtensions(target)[[PreventExtensions]]target: 目标对象禁止对象扩展时拦截的操作Object.preventExtensions
getOwnPropertyDescriptor(target, property)[[GetOwnProperty]]target: 目标对象 property: 要获取描述符的属性获取对象属性描述符时拦截的操作Object.getOwnPropertyDescriptor,For ...in/ Object.keys()/values()/entries
defineProperty(target, property, descriptor)[[DefineOwnProperty]]target: 目标对象 property: 要定义的属性 descriptor: 属性描述符对象定义对象属性时拦截的操作Object.defineProperty/Object.defineProperties
ownKeys(target)[[HasProperty]]target: 目标对象获取对象所有属性名的操作Object.getOwnPropertyNames,Object.getOwnPropertySymbols,for ...in/ Object.keys()/values()/entries
enumerate(target)target: 目标对象for...in的形式枚举对象属性名时拦截的操作

Proxy的局限性

无法完整代理Map, Set, Date, Promise等,因为他们都使用了内部插槽,Proxy原理是将proxy对象上的内部方法转发到target对象上,当target为Map, Set, Date, Promise,Proxy并不能转发他们使用内部插槽,而且Proxy仅支持转发普通对象的内部方法

关于为什么只转发普通对象内部方法,创建一个Proxy对象会执行抽象操作MakeBasicObject,这个操作会将传入对象的内部方法为普通对象才存在的内部方法具体可以看这个定义(tc39.es/ecma262/#se…

下面是不能被代理的部分例子

  • Map 将数据存储在[[MapData]]中,他并不是一个普通对象内部插槽
  • Set -> [[SetData]]
  • Promise -> [[AlreadyCalled]]
  • Date -> [[DateValue]]

Proxy ECMAScript 2023 tc39.es/ecma262/#se… 这个规范说明了proxy可以代理的内部方法,基本能够与Proxy支持的traps对应

代理透明性

对于普通对象,被代理的对象,与原始对象调用同一个方法,或获取同一个属性,结果应该是相同的

let target = {}
const o = new Proxy(target, {})
// 两者结果是一致的
target.fn() === o.fn()

可撤销的Proxy

一个 可撤销 的代理是可以被禁用的代理。可以随时禁止对proxy对象的访问

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// 将 proxy 传递到其他某处,而不是对象...
alert(proxy.data); // Valuable data

// 稍后,在我们的代码中
revoke();

// proxy 不再工作(revoked)
alert(proxy.data); // Error

Reflect

Reflect给我们在代码中调用这些内部方法提供了可能。

【磨破嘴皮】Proxy & Reflect

对于Proxy中的拦截属性,每一个Reflect都有一个与之对应的方法,他的名称和参数都与Proxy的拦截器相同,所以我们在处理Proxy拦截器中对原始对象的操作时,可以直接

基本使用

const user = {}

const status = Reflect.set(user, 'name', 'xiaoming')

console.log(user.name, status) // xiaoming, true

getter代理

let user = {
  _name: "user1",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {get(target, prop, receiver) {return target[prop];}});

直接使用target[prop]返回,用于获取值是足够的,但与方法一样,有一些复杂的操作可能会导致出现问题,比如继承

const user2 = {
    __proto__: userProxy,
    _name: 'user2'
}
// 并不会输出 user2
user2.name // user1

target始终指向被代理的原始对象,所以这里会返回user1,我们可以使用Reflect将上下文传递给get

userProxy = new Proxy(user, {
    get(target, prop, receiver) {
        // receiver => user2
        // target => user1
        return Reflect.get(target, prop, receiver)
    }
})
// 正确输出user2
user2.name // user2

Qiankun Proxy沙箱

qiankun.js中共有三种沙箱LegacySandbox,ProxySandBox,SnapshotSandBox

分别对应:只需要单个子应用活跃,多个子应用活跃,不支持proxy浏览器的场景

LegacySandbox

他是一个不够纯粹的沙箱,在设置值时,记录变化,在卸载应用时,将window恢复原样,会直接修改到真正的window对象,当存在多个子应用时,有可能会有冲突,所以他不支持多个子应用同时运行。

    class LegacySandbox {
        // 持续记录新增和修改的全局变量
        currentUpdatePropsValueMap = new Map()
        // 沙箱期间更新的全局变量
        modifiedPropsOriginalValueMapInSandbox = new Map()
        // 沙箱期间新增的全局变量
        addedPropsMapInSandbox = new Map()
        propsWindow = {}

        // 核心逻辑
        constructor() {
            const fakeWindow = Object.create(null)
            // 设置值或者获取值
            this.propsWindow = new Proxy(fakeWindow, {
                set: (target, prop, value, receiver) => {
                    const originValue = window[prop]
                    if (!window.hasOwnProperty(prop)) {
                        this.addedPropsMapInSandbox.set(prop, value)
                    } else if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
                        this.modifiedPropsOriginalValueMapInSandbox.set(prop, originValue)
                    }
                    this.currentUpdatePropsValueMap.set(prop, value)
                    window[prop] = value
                },
                get: (target, prop, receiver) => {
                    return window[prop]
                }
            })

        }
        setWindowProp(prop, value, isToDelete) {
            if (value === undefined && isToDelete) {
                delete window[prop]
            } else {
                window[prop] = value
            }

        }

        active() {
            // 恢复上一次该微应用处于运行状态时,对window 上做的所有应用的修改
            this.currentUpdatePropsValueMap.forEach((value, prop) => {
                this.setWindowProp(prop, value)
            })
        }
        // 失活 
        inactive() {
            // 还原window上的属性
            this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
                this.setWindowProp(prop, value)
            })
            // 删除在微应用运行期间 window 新增的属性
            this.addedPropsMapInSandbox.forEach((_, prop) => {
                this.setWindowProp(prop, undefined, true)
            })
        }
    }

    const sandBox = new LegacySandbox()

    window.name = '张铁蛋'

    sandBox.active()
    // 这个操作会被直接代理到真实window上
    sandBox.propsWindow.name = '王麻子'
    console.log(window.name) // 王麻子
    sandBox.inactive() // 失活恢复原本的 window
    console.log(window.name) // 张铁蛋

ProxySandBox

proxysandbox就是用于解决LegacySandbox只能应用于单例场景的问题,他使用proxy代理,将变化存储在updateValueMap中,取值时,会优先从fakewindow获取,如果没有再访问真实window。

    class ProxySandbox {
        proxyWindow
        isRunning = false
        active() {
            this.isRunning = true
        }
        inactive() {
            this.isRunning = false
        }
        constructor() {
            const fakeWindow = Object.create(null)
            this.proxyWindow = new Proxy(fakeWindow, {
                set: (target, prop, value, receiver) => {
                    if (this.isRunning) {
                        target[prop] = value
                    }
                },
                get: (target, prop, receiver) => {
                    return prop in target ? target[prop] : window[prop];
                }
            })
        }
    }
    const windowProxy1 = new ProxySandBox()
    const windowProxy2 = new ProxySandBox()
    windowProxy1.proxyWindow.name = 'windowProxy1'
    windowProxy2.proxyWindow.name = 'windowProxy2'
    window.windowName = 'window-global'
    windowProxy1.windowName // 'window-global'
    windowProxy2.windowName // 'window-global'
    windowProxy1.proxyWindow.name // 'windowProxy1'
    windowProxy2.proxyWindow.name // 'windowProxy2'

Vue中的Proxy

Vue3使用Proxy来实现依赖收集,并触发effect,对比Vue2使用defineProperty(),解决了以下问题

  • 对象新增属性无法被监听

  • 无法监听数组的变化

    • 通过重写数组的push\prop\shift\unshift\splice\sort\reverse实现监听数组变化
  • 无法一次性监听对象的子属性,需要递归调用defineProperty实现

  • 只能拦截get和set操作

核心代码

const mutableHandlers:ProxyHandler<Record<any, any>> = {
    // 取值
    // 会手机对象在那个effect中
    get(target, key, recevier) {
        // 当发现是对isReactive的判断取值时,则直接返回true
        if(key === ReactiveFlags.IS_REACTIVE) {
            return true
        }
        // 收集依赖
        track(target, key)
        console.log('obj get', target, key)
        // recevier 代表代理对象本身
        const res = Reflect.get(target, key, recevier)
        return res
    },
    set(target, key, value, recevier) {
        let oldValue = (target as any)[key];
        const res = Reflect.set(target, key, value, recevier)
        // 当值发生变化时,触发effect再次执行
        if(oldValue !== value) {
            trigger(target, key)
        }
        // 设置成功时返回 true
        return res
    }
}
// 弱引用,当对象没有引用时,会被自动销毁
const reactiveMap = new WeakMap();

function createReactiveObject(target:object) {
    if((target as any)[ReactiveFlags.IS_REACTIVE]) {
        return target
    }
    if(!isObject(target)) {
        return target
    }
    // 对已经代理过的结果进行缓存
    const exisitingProxy = reactiveMap.get(target);

    if(exisitingProxy) {
        return exisitingProxy
    }

    const proxy = new Proxy(target, mutableHandlers) //当用户获取属性时,能够截取到

    reactiveMap.set(target, proxy)

    return proxy
}

Proxy的性能

我们可以利用Performance来测试defineProperty, 与proxy代理get/set方法的耗时

const proxyObj = new Proxy({name: '1'}, {})
let defineProp = {name: '1'}
let val = ''
const definePropObj= Object.defineProperty(defineProp, 'name', {
    set(v) {
        val = v
    }
})
function doCount(callback, time=10 * 10000, ) {
    while(time--) {
        callback && callback(time)
    }
}
let markProxystart = performance.mark('proxystart')
doCount((time)=>{
    proxyObj.name = time
})
let markProxyend = performance.mark('proxyend')

let markDefinestart = performance.mark('proxyDefinestart')
doCount((time)=>{
    definePropObj.name = time
})
let markDefineend = performance.mark('proxyDefineend')

console.log(performance.measure('proxyTime', 'proxystart', 'proxyend'))
console.log(performance.measure('defineTime', 'proxyDefinestart', 'proxyDefineend'))

【磨破嘴皮】Proxy & Reflect

可以看到Proxy代理的对象,进行赋值操作,会比defineProperty慢一半以上

扩展

内部插槽

内部槽是JavaScript对象或规范类型的数据成员。它们用于存储对象的状态。所有的对象都拥有以下两个内置插槽

[[Prototype]]:实现继承,为null表示没有继承关系

[[Extensible]]:是否能被扩展, 默认为true,对应方法Object.preventExtensions()可改为false, 为false时,不能修改回true,不能在对象上新增属性,不能修改[[Prototype]]插槽

内部方法

普通对象内部方法

要将内部方法,首先需要理解抽象操作这个概念,抽象操作是由ECMAScript规范中定义的函数,目的是让一些冗长的操作或代码实现逻辑、有一个代词,例如我们常用的互联网黑话(组合拳、私域、闭环之类)。主要是方便规范编写。这些抽象方法就是内部方法/内部插槽的主要使用者。

而内部方法是JavaScript对象的成员函数。在ECMAScript中,对象的实际意义是通过成为内部方法的算法来指定的,每个对象都与一组内部方法相关联,这些内部方法定义了对象运行时的行为。但是内部方法并不是ECMAScript语言的一部分,只是一个语言实现的规范,内部实现由具体的实现由实现此规范的编码人员决定。

[[GetPrototypeOf]] - object.getPrototypeOf()

内部方法是多态的,在运行时,编码实现尝试调用对象不支持的内部方法,则会抛出TypeError异常,例如使用null就不能调用getPrototypeOf, 这个方法对应以下4个步骤

【磨破嘴皮】Proxy & Reflect

  1. null调用getPrototypeOf()
  2. 调用内部方法[[GetPrototypeOf]]
  3. 内部方法[[GetPrototypeOf]]委托给抽象操作OrdinaryGetPrototypeOf
  4. OrdinaryGetPrototypeOf尝试返回内部插槽null.[[Prototype]] (null.prototype )

而null是没有prototype的

【磨破嘴皮】Proxy & Reflect

[[SetPrototypeOf]] ( V ) -> Object.setPrototypeOf(v)

返回调用 ! OrdinarySetPrototypeOf(O, V)结果

[[IsExtensible]] ( ) -> Object.isExtensible(O)

返回OrdinaryIsExtensible(O)

[[PreventExtensions]] ( )

返回 OrdinaryPreventExtensions(O).

将对象的extensible属性设置为false

[[GetOwnProperty]] ( P )

返回OrdinaryGetOwnProperty(O, P),会执行以下步骤

  1. 检查O是否有P属性,如果没有返回undefined

  2. 新建一个空属性描述对象D

  3. 在O新建一个X变量,键为P

  4. 检查X是否为字面量

  5. 是:

    1. D.[[Value]] = X.[[Value]]
    2. D.[[Writable]] = X.[[Writable]]
  6. 否:

    1. 断言X为一个访问器属性
    2. D.[[Get]] = X.[[Get]]
    3. D.[[Set]] = X.[[Set]]
    4. D.[[Enumerable]] = X.[[Enumerable]]
    5. D.[[Configurable]] = X.[[Configurable]]
  7. 返回 D

[[DefineOwnProperty]] ( P, Desc )

返回 OrdinaryDefineOwnProperty(O, P, Desc)

  1. 断言:V 的类型要么是对象要么是 Null.

  2. 定义 current 变量,值为 O.[[Prototype]] .

  3. 如果 SameValue(V, current) 结果为 true,返回 true

  4. 定义 extensible 变量,值为 O.[[Extensible]] .

  5. 如果 extensiblefalse,返回 false.(不可扩展

  6. 定义 p 变量,值为 V.

  7. 定义 done,值为 false.

  8. 如果 donefalse 的话,重复以下步骤:

  9. 8.1 如果 pnull,将 done 设为 true

  10. 8.2 否则如果 SameValue(p, O) 结果为 true,返回 false这一步防止对象将自身设为自己的原型对象

  11. 8.3 否则,

    1. 如果 p.[[GetPrototypeOf]] 不是普通对象内部方法,将 done 设为 true
    2. 否则,把 p.[[Prototype]] 的值设为 p
  12. O.[[Prototype]] 的值设为 V

  13. 返回 true

步骤8中的循环保证在任何原型链中都不会有循环,该原型链仅包括使用 [[GetPrototypeOf]][[SetPrototypeOf]] 的普通对象定义的对象。

转载自:https://juejin.cn/post/7268658252567625786
评论
请登录