let data = [1,2,3] let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.unshift('a') // get value: unshift // get value: length // get value: 2 // set value: 3 3 // get value: 1 // set value: 2 2 // get value: 0 // set value: 1 1 // set value: 0 a // set value: length 4
可以看到,在对数组做 unshift 操作时,会多次触发 get 和 set 。仔细观察输出,不难看出,get 先拿数组最末位下标,开辟新的下标 3 存放原有的末位数值,然后再将原数值都往后挪,将 0 下标设置为了 unshift 的值 a ,由此引发了多次 set 操作。
而这对于 通知外部操作 显然是不利,我们假设 set 中的 console 是触发外界渲染的 render 函数,那么这个 unshift 操作会引发 多次 render 。
我们后面会讲述如何解决相应的这个问题,继续。
细节三:proxy 只能代理一层
let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] } let p = new Proxy(data, { get(target, key, receiver) { console.log('get value:', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set value:', key, value) return Reflect.set(target, key, value, receiver) } }) p.bar.key = 2 // get value: bar
执行代码,可以看到并没有触发 set 的输出,反而是触发了 get ,因为 set 的过程中访问了 bar 这个属性。
由此可见,proxy 代理的对象只能代理到第一层,而对象内部的深度侦测,是需要开发者自己实现的。同样的,对于对象内部的数组也是一样。
p.ary.push('c') // get value: ary
同样只走了 get 操作,set 并不能感知到。
我们注意到 get/set 还有一个参数:receiver ,对于 receiver ,其实接收的是一个代理对象:
let data = { a: {b: {c: 1 } } } let p = new Proxy(data, { get(target, key, receiver) { console.log(receiver) const res = Reflect.get(target, key, receiver) return res }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } }) // Proxy {a: {…}}
这里 receiver 输出的是当前代理对象,注意,这是一个已经代理后的对象。
let data = { a: {b: {c: 1 } } } let p = new Proxy(data, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver) console.log(res) return res }, set(target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } }) // {b: {c: 1} }
当我们尝试输出 Reflect.get 返回的值,会发现,当代理的对象是多层结构时,Reflect.get 会返回对象的内层结构。
记住这一点,Vue3 实现深度的proxy ,便是很好的使用了这点。
解决 proxy 中的细节问题
前面提到了使用 Proxy 来侦测数据变化,有几个细节问题,包括:
使用 Reflect 来返回 trap 默认行为
对于 set 操作,可能会引发代理对象的属性更改,导致 set 执行多次
proxy 只能代理对象中的一层,对于对象内部的操作 set 未能感知,但是 get 会被执行
接下来,我们将先自己尝试解决这些问题,后面再分析 Vue3 是如何解决这些细节的。
setTimeout 解决重复 trigger
function reactive(data, cb) { let timer = null return new Proxy(data, { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { clearTimeout(timer) timer = setTimeout(() => { cb && cb() }, 0); return Reflect.set(target, key, value, receiver) } }) } let ary = [1, 2] let p = reactive(ary, () => { console.log('trigger') }) p.push(3) // trigger
程序输出结果为一个: trigger
这里实现了 reactive 函数,接收两个参数,第一个是被代理的数据 data ,还有一个回调函数 cb,我们这里先简单的在 cb 中打印 trigger 操作,来模拟通知外部数据的变化。
解决重复的 cb 调用有很多中方式,比方通过标志,来决定是否调用。而这里是使用了定时器 setTimeout ,每次调用 cb 之前,都清除定时器,来实现类似于 debounce 的操作,同样可以解决重复的 callback 问题。
解决数据深度侦测
目前还有一个问题,那便是深度的数据侦测,我们可以使用递归代理的方式来实现: