• 【vue设计与实现】非原始值的响应式方案 10-如何代理Set和Map


    Set和Map类型的数据有特定的属性和方法来操作自身,这点和普通对象十分不同。因此不能像代理普通对象那样代理Set和Map类型的数据。当然总体的思路还是一样,读取操作时,调用track建立响应联系;设置操作时,调用trigger触发响应。
    那么在实现之前,有必要先了解使用Proxy代理Set以及Map要注意的地方。
    先来看一段代码:

    const s = new Set([1,2,3])
    const p = new Proxy(s,{})
    console.log(p.size)
    
    • 1
    • 2
    • 3

    结果报错

    Uncaught TypeError: Method get Set.prototype.size called on incompatible receiver #<Set>
        at get size (<anonymous>)
        at <anonymous>:3:15
    
    • 1
    • 2
    • 3

    通过参阅规范得知,Set.prototype.size是一个访问器属性,在上例子中作为方法调用,并且size的set函数是undefined,其get函数会执行一下步骤:
    在这里插入图片描述
    关键点在第一步和第二步
    首先第一步中Let S be the this value(设置S的值为this) 这里this指的是什么,由于是通过代理对象来访问size属性的,所以this就是代理对象p
    然后第二步,Perform ? RequireInternalSlot(S, [[SetData]]) (调用抽象方法RequireInternalSlot(S, [[SetData]]) )来检查S是否存在内部槽[[SetData]]。显然代理对象S没有[[SetData]]这个内部槽,所以会抛出错误

    为了修复这个问题,需要修改访问器的getter函数执行时的this指向,如下面代码所示

    const s = new Set([1,2,3])
    const p = new Proxy(s,{
    	get(target, key, receiver){
    		if(key==='size'){
    			// 如果读取的是size属性
    			// 通过指定第三个参数receiver为原始对象target从而修复问题
    			return Reflect.get(target, key, target)
    		}
    		// 读取其他属性则是默认行为
    		return Reflect.get(target, key, target)
    		
    	}
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    接着尝试从Set中删除数据,如下面代码所示:

    const s = new Set([1,2,3])
    const p = new Proxy(s,{
    	get(target, key, receiver){
    		if(key === 'size'){
    			return Reflect.get(target, key, target)
    		}
    		return Reflect.get(target, key, receiver)
    	}
    })
    
    p.delete(1)
    // 结果会报错
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    Uncaught TypeError: Method Set.prototype.delete called on incompatible receiver #<Set>
        at Proxy.delete (<anonymous>)
        at <anonymous>:11:9
    
    • 1
    • 2
    • 3

    这个错误和p.size报错的错误很相似,但是实际上,访问p.size与访问p.delete是不同的,size是一个属性而delete是一个方法,访问p.size时,getter函数会立即执行;而访问p.delete时,delete方法没有执行,真正执行的是p.delete(1)这句函数调用。因此无论怎么修改receiver,delete方法执行时的this都是指向代理对象p。要解决这个问题,就要把delete方法和原始数据对象绑定即可。代码如下:

    const s = new Set([1,2,3])
    const p = new Proxy(s,{
    	get(target, key, receiver){
    		if(key === 'size'){
    			return Reflect.get(target, key, target)
    		}
    		// 将方法雨原始数据对象target绑定后返回
    		return target[key].bind(target)
    	}
    })
    
    p.delete(1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在上面代码中,使用target[key].bind(target)代替了Reflect.get(target,key,receiver)。使用bind函数将用于操作数据的方法和原始数据对象target做了绑定,这p.delete(1)执行时,delete函数的this总是指向数据对象而非代理对象。

    最后将上面的代码封装到前面提到的createReactive函数中

    const reactiveMap = new Map()
    function reactive(obj){
    	const proxy = createReactive(obj)
    	const existionProxy = reactiveMap.get(obj)
    	reactiveMap.set(obj, proxy)
    	return proxy
    }
    
    // 在createReactive里封装用于代理Set/Map类型数据的逻辑
    function createReactive(obj, isShallow = false, isReadonly = false){
    	return new Proxy(obj, {
    		get(target, key, receiver){
    			if(key === 'size'){
    				return Reflect.get(target, key, target)
    			}
    			// 将方法雨原始数据对象target绑定后返回
    			return target[key].bind(target)
    		}	
    	}
    })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这样就可以创建代理数据了

    const p = reactive(new Set([1,2,3]))
    console.log(p.size) // 3
    
    • 1
    • 2

    知识扩展:
    关于访问器属性
    js有两种属性类型
    数据属性:一般用于存储数据数值
    访问器属性一般进行get和set操作,不能直接存储数据数值

    访问器属性

    • 不包括数据值
    • 包含set和get函数
    • 读取访问器属性,get函数返回有效值
    • 写入访问器属性,set函数处理数据
    • 不能直接定义,必须使用defineProperty定义

    规范地址:https://tc39.es/ecma262/

  • 相关阅读:
    (01)ORB-SLAM2源码无死角解析-(54) 闭环线程→闭环检测:寻找闭环候选关键帧 LoopClosing::DetectLoop()
    UNCTF-日常训练-reverse-ezRust
    Lock锁之公平锁与非公平锁(AQS实现原理)
    基于python技术的酒店管理系统
    css实现卡片突出焦点功能
    Spring 面试题及答案整理,最新面试题
    Pegasus智能家居套件样例开发--软定时器
    SQL Server 2014安装笔记
    基于Java毕业设计大学生兼职招聘网站源码+系统+mysql+lw文档+部署软件
    react-react-dom v6 知识整合
  • 原文地址:https://blog.csdn.net/loyd3/article/details/125347948