function reactive(data, cb) { let res = null let timer = null res = data instanceof Array ? []: {} for (let key in data) { if (typeof data[key] === 'object') { res[key] = reactive(data[key], cb) } else { res[key] = data[key] } } return new Proxy(res, { get(target, key) { return Reflect.get(target, key) }, set(target, key, val) { let res = Reflect.set(target, key, val) clearTimeout(timer) timer = setTimeout(() => { cb && cb() }, 0) return res } }) } let data = { foo: 'foo', bar: [1, 2] } let p = reactive(data, () => { console.log('trigger') }) p.bar.push(3) // trigger
对代理的对象进行遍历,对每个 key 都做一次 proxy,这是递归实现的方式。同时,结合前面提到的 timer 避免重复 set 的问题。
这里我们可以输出代理后的对象 p :
可以看到深度代理后的对象,都携带 proxy 的标志。
到这里,我们解决了使用 proxy 实现侦测的系列细节问题,虽然这些处理方式可以解决问题,但似乎并不够优雅,尤其是递归 proxy 是一个性能隐患,当数据对象比较大时,递归的 proxy 会消耗比较大的性能,并且有些数据并非需要侦测,我们需要对数据侦测做更细的控制。
接下来我们就看下 Vue3 是如何使用 Proxy 实现数据侦测的。
Vue3 中的 reactivity
Vue3 项目结构采用了 lerna 做 monorepo 风格的代码管理,目前比较多的开源项目切换到了 monorepo 的模式,比较显著的特征是项目中会有个 packages/ 的文件夹。
Vue3 对功能做了很好的模块划分,同时使用 TS 。我们直接在 packages 中找到响应式数据的模块:
其中,reactive.ts 文件提供了 reactive 函数,该函数是实现响应式的核心。同时这个函数也挂载在了全局的 Vue 对象上。
这里对源代码做一点程度的简化:
const rawToReactive = new WeakMap() const reactiveToRaw = new WeakMap() // utils function isObject(val) { return typeof val === 'object' } function hasOwn(val, key) { const hasOwnProperty = Object.prototype.hasOwnProperty return hasOwnProperty.call(val, key) } // traps function createGetter() { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver) return isObject(res) ? reactive(res) : res } } function set(target, key, val, receiver) { const hadKey = hasOwn(target, key) const oldValue = target[key] val = reactiveToRaw.get(val) || val const result = Reflect.set(target, key, val, receiver) if (!hadKey) { console.log('trigger ...') } else if(val !== oldValue) { console.log('trigger ...') } return result } // handler const mutableHandlers = { get: createGetter(), set: set, } // entry function reactive(target) { return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, ) } function createReactiveObject(target, toProxy, toRaw, baseHandlers) { let observed = toProxy.get(target) // 原数据已经有相应的可响应数据, 返回可响应数据 if (observed !== void 0) { return observed } // 原数据已经是可响应数据 if (toRaw.has(target)) { return target } observed = new Proxy(target, baseHandlers) toProxy.set(target, observed) toRaw.set(observed, target) return observed }
rawToReactive 和 reactiveToRaw 是两个弱引用的 Map 结构,这两个 Map 用来保存 原始数据 和 可响应数据 ,在函数 createReactiveObject 中,toProxy 和 toRaw 传入的便是这两个 Map 。
我们可以通过它们,找到任何代理过的数据是否存在,以及通过代理数据找到原始的数据。
除了保存了代理的数据和原始数据,createReactiveObject 函数仅仅是返回了 new Proxy 代理后的对象。重点在 new Proxy 中传入的handler参数 baseHandlers。
还记得前面提到的 Proxy 实现数据侦测的细节问题吧,我们尝试输入:
let data = { foo: 'foo', ary: [1, 2] } let r = reactive(data) r.ary.push(3)
打印结果:
可以看到打印输出了一次 trigger ...
问题一:如何做到深度的侦测数据的 ?