网上介绍 reactive 的各种资料也是非常多了,这里按照官网的介绍,对reactive以及相关的几个函数,做一个综合介绍,另外加一些扩展和稍微深入一点的内容。
ES6的Proxy
Proxy是ES6提供的一个可以拦截对象基础操作的代理。因为 reactive 采用Proxy代理的方式,实现引用类型的响应性,所以我们先看看 Proxy 的基础使用方法,以便于我理解 reactive 的结构。我们先来定义一个函数,了解一下 Proxy 的基本使用方式:
// 定义一个函数,传入对象原型,然后创建一个Proxy的代理 const myProxy = (_target) => { // 定义一个 Proxy 的实例 const proxy = new Proxy(_target, { // 拦截 get 操作 get: function (target, key, receiver) { console.log(`getting ${key}!`, target[key]) // 用 Reflect 调用原型方法 return Reflect.get(target, key, receiver) }, // 拦截 set 操作 set: function (target, key, value, receiver) { console.log(`setting ${key}:${value}!`) // 用 Reflect 调用原型方法 return Reflect.set(target, key, value, receiver) } }) // 返回实例 return proxy } // 使用方法,是不是和reactive有点像? const testProxy = myProxy({ name: 'jyk', age: 18, contacts: { QQ: 11111, phone: 123456789 } }) console.log('自己定义的Proxy实例:') console.log(testProxy) // 测试拦截情况 testProxy.name = '新的名字' // set操作 console.log(testProxy.name) // get 操作
Proxy有两个参数target和handle。* target:要代理的对象,也可以是数组,但是不能是基础类型。* handler:设置要拦截的操作,这里拦截了set和get操作,当然还可以拦截其他操作。
我们先来看一下运行结果:
- Handler 可以看到我们写的拦截函数 get 和 set;
- Target 可以看到对象原型。
注意:这里只是实现了 get 和 set 的拦截,并没有实现数据的双向绑定,模板也不会自动更新内容,Vue内部做了很多操作才实现了模板的自动更新功能。
用 Proxy 给 reactive 套个娃,会怎么样?
有个奇怪的地方,既然Proxy可以实现对set等操作的拦截,那么 reactive 为啥不返回一个可以监听的钩子呢?为啥要用watch来实现监听的工作呢?为啥会这么想?看看Vuex4.0的设计,明明已经把state整体自动变成了 reactive 的形式,那么为啥还非得在 mutations 里写函数,实现 set 操作呢?好麻烦的样子。外部直接对 reactive 进行操作,然Vuex内部监听一下,这样大家不就都省事了吗?要实现插件功能,还是跟踪功能,不都是可以自动实现了嘛。所以我觉得还是可以套个娃的。
实现模板的自动刷新本来以为上面那个 myProxy 函数,传入 一个 reactive 之后,就可以自动实现更新模板的功能了,结果模板没理我。这不对呀,我只是监听了一下,不是又交给reactive了吗?为啥模板不鸟我?经过各种折腾,终于找到了原因,于是函数改成了这样:
/** * 用 Proxy定义一个 reactive 的套娃,实现可以监听任意属性变化的目的。(不包含嵌套对象的属性) * @param {*} _target 要拦截的目标 * @param {*} callback 属性变化后的回调函数 */ const myReactive = (_target, callback) => { let _change = (key, value) => {console.log('内部函数')} const proxy = new Proxy(_target, { get: function (target, key, receiver) { if (typeof key !== 'symbol') { console.log(`getting ${key}!`, target[key]) } else { console.log('getting symbol:', key, target[key]) } // 调用原型方法 return Reflect.get(target, key, receiver) }, set: function (target, key, value, receiver) { console.log(`setting ${key}:${value}!`) // 源头监听 if (typeof callback === 'function') { callback(key, value) } // 任意位置监听 if (typeof _target.__watch === 'function') { _change(key, value) } // 调用原型方法 return Reflect.set(target, key, value, target) } }) // 实现任意位置的监听, proxy.__watch = (callback) => { if (typeof callback === 'function') { _change = callback } } // 返回实例 return proxy }
代码稍微多了一些,我们一块一块看。
- get 这里要做一下 symbol 的判断,否则会报错。好吧,其实我们似乎不需要 console.log。
- set 这里改了一下最后一个参数,这样模板就可以自己更新了。
- 设置 callback 函数,实现源头监听 设置一个回调函数,才能在拦截到set操作的时候,通知外部的调用者。只是这样只适合于定义实例的地方。那么接收参数的地方怎么办呢?
调用方法如下:
// 定义一个拦截reactive的Proxy // 并且实现源头的监听 const myProxyReactive = myReactive(retObject, ((key, value) =>{ console.log(`ret外部获得通知:${key}:${value}`) }) )
这样我们就可以在回调函数里面得到修改的属性名称,以及属性值。这样我们做状态管理的时候,是不是就不用特意去写 mutations 里面的函数了呢?
- 内部设置一个钩子函数 设置一个 _change() 钩子函数,这样接收参数的地方,可以通过这个钩子来得到变化的通知。
调用方法如下:
// 任意位置的监听 myProxyReactive.__watch((key, value) => { console.log(`任意位置的监听:${key}:${value}`) })
只是好像哪里不对的样子。首先这个钩子没找到合适的地方放,目前放在了原型对象上面,就是说破坏了原型对象的结构,这个似乎会有些影响。然后,接收参数的地方,不是可以直接得到修改的情况吗?是否还需要做这样的监听?
最后,好像没有watch的deep监听来的方法,那么问题又来了,为啥Vuex不用watch呢?或者悄悄的用了?
深层响应式代理:reactive
说了半天,终于进入正题了。reactive 会返回对象的响应式代理,这种响应式转换是深层的,可以影响所有的嵌套对象。
注意:返回的是 object 的代理,他们的地址是相同的,并没有对object进行clone(克隆),所以修改代理的属性值,也会影响原object的属性值;同时,修改原object的属性值,也会影响reactive返回的代理的属性值,只是代理无法拦截直接对原object的操作,所以模板不会有变化。
这个问题并不明显,因为我们一般不会先定义一个object,然后再套上reactive,而是直接定义一个 reactive,这样也就“不存在”原 object 了,但是我们要了解一下原理。我们先定义一个 reactive 实例,然后运行看结果。
// js对象 const person = { name: 'jyk', age: 18, contacts: { QQ: 11111, phone: 123456789 } } // person 的 reactive 代理 (验证地址是否相同) const personReactive = reactive(person) // js 对象 的 reactive 代理 (一般用法) const objectReactive = reactive({ name: 'jykReactive', age: 18, contacts: { QQ: 11111, phone: 123456789 } }) // 查看 reactive 实例结构 console.log('reactive', objectReactive ) // 获取嵌套对象属性 const contacts = objectReactive .contacts // 因为深层响应,所以依然有响应性 console.log('contacts属性:', contacts) // 获取简单类型的属性 let name = objectReactive.name // name属性是简单类型的,所以失去响应性 console.log('name属性:', name)
运行结果:
- Handler:可以看到Vue除重写set和get外,还重写了deleteProperty、has和ownKeys。
- Target:指向一个Object,这是建立reactive实例时的对象。
属性的结构:
然后再看一下两个属性的打印结果,因为contacts属性是嵌套的对象,所以单独拿出来也是具有响应性的。而name属性由于是string类型,所以单独拿出来并不会自动获得响应性,如果单独拿出来还想保持响应性的话,可以使用toRef。
注意:如果在模板里面使用{{personReactive.name}}的话,那么也是有响应性的,因为这种用法是获得对象的属性值,可以被Proxy代理拦截,所以并不需要使用toRef。如果想在模板里面直接使用{{name}}并且要具有响应性,这时才需要使用toRef。