响应式:简而言之,就是当数据发生改变的时候,视图会重新渲染,更新为最新的值。
初识Object.defineProperty
Object.defineProperty()
的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
Object.defineProperty(obj, prop, desc)
- obj 需要定义属性的当前对象
- prop 当前需要定义的属性名
- desc 属性描述符
Object.defineProperty是 Vue 响应式系统的精髓。
Vue使用 Object.defineProperty
为对象中的每一个属性,设置 get 和 set 方法,进行数据劫持/监听;
get 值是一个函数,当属性被访问时,会触发 get 函数
set 值同样是一个函数,当属性被赋值时,会触发 set 函数
const data = {}; let name = 'Vueeee' // 在data对象中定义name属性 Object.defineProperty(data,'name',{ // 当访问data.name时自动调用此函数 get(){ console.log('🚀- 我是get'); return name }, // 当赋值data.name时自动调用此函数 set(newValue){ console.log('🚀- 我是set'); name = newValue // vue会接着做一个视图重新渲染的操作 } }) // 调用了data.name属性的get方法 console.log(data.name) // 调用了data.name属性的set方法 data.name = 'Hyyyyy' console.log(data.name)
基本的响应式实现
上面说道可以使用Object.defineProperty
来实现vue当中的响应式。 现在来使用Object.defineProperty
来实现一个mini的vue响应式
的例子
// 如果在vue中我们只需要这样书写,即是响应式数据。 export default { data() { return{ name: 'Hyyy', age: 23, } } }
那么vue是怎么做到呢?我们来写个简单的例子
// 首先模拟vue2中data const data = { name: 'Hyyy', age: 23, } // 使data对象 变成响应式数据 observer(data) // 为传入的对象做响应式 function observer(target){ // 只处理对象 if(typeof target !== 'object' || target === null) return target // 遍历,为对象中的每一个属性,设置 get 和 set 方法,进行数据劫持/监听 for(let key in target){ // 传入Object.defineProperty()方法 所需要的对象本身、key和value defineReactive(target, key, target[key]) } } // 为传入的对象做数据劫持/监听 function defineReactive(target, key, value){ Object.defineProperty(target, key, { get(){ return value }, set(newValue){ // 如果当前value不等于 if(value !== newValue){ value = newValue console.log('触发set,🔥更新视图操作') } } }) } data.name = 'yHhhhhhhh'
上面的例子就是如何使用Object.defineProperty()
来实现一个定义mini响应式数据的过程。
vue的源码肯定更加复杂,会判断各种情况,但核心就是这样了.
处理值为复杂对象的情况
上面我们只处理了最简单的情况,对象中的属性只是数字、字符串
如果是复杂的对象又该如何呢?
const data = { name: 'Hyyy', age: 23, // 如果我们加上个对象 friend: { friendName: 'xxx', } } function observer(){ /* 之前代码 */ } function defineReactive(){ /* 之前代码 */ } // 对新加入的friend属性中的name属性进行更改 data.friend.name = 'xxx2号
此时会发现控制台中并没有和上个demo一样,出现'触发set,🔥更新视图操作'
这句log
此时如果我们在上面定义的defineReactive
方法中console.log
打印传来的key,会发现
function defineReactive(target, key, value){ console.log(key) -> 只能打印出data对象中的 name、age、friend三个属性 //并不能打印出friend属性中的firendName属性,所以我们其实是没有给对象中的属性做数据监听的 Object.defineProperty(target, key, { /* 之前代码 */ }) } 复制代码
如何达到对象中的属性也可以监听到呢?简单,只需要在defineReactive()
函数中加入一行observer(value)
...
function defineReactive(target, key, value){ // 深度观察,只需要将当前对象传给observer,也做个监听就好了 observer(value) Object.defineProperty(target, key, { /* 之前代码 */ }) 复制代码
但还没有完!!!
如果我们将data对象中的属性,赋值为一个新的对象,那这个对象还是没有受到监听的...
例如
const data = { age: 23, /* 之前代码 */ } function observer(){ /* 之前代码 */ } function defineReactive(){ /* 之前代码 */ } data.age = { number: 23} // 会触发一次更新 //此时如果我们.. data.age.number = 21 // 又更改了,想想中会再触发一次更新
但并没有,只有data.age = { number: 23}
触发了更新,因为我们没有监听data.age.number
所以我们在set的时候,也就是data.age = { number: 23}
的时候,也要在set方法中对新传来的对象{number: 23}
进行监听
set(newValue){ // 加上这句!!! observer(newValue) /* 之前代码 */ /* 之前代码 */ /* 之前代码 */ /* 之前代码 */ } /* 之前代码 */ /* 之前代码 */ data.age = { number: 23} // 会触发一次更新 data.age.number = 21 // 再触发一次更新
也就是我们对对象中的对象属性也要进行深度的监听,才能在数据改变的时候及时更新。
所以这也是我们再使用Object.defineProperty()
做响应式的一个问题,即使我们数据是个层级很深的对象,他也会在一开始对所有数据不断的深度监听,直到他是个普通的值为止。
所以Vue3中, 改用了Proxy来解决,Proxy就会在使用到这个数据的时候,才会去做这个监听的过程。
除了上述问题,如果我们做如下操作:
delete data.某属性 data.新属性 = 'xxx'
并不会被响应到,因为Object.defineProperty()
是没法处理属性删除与属性新增的。
所以在vue中删除我们会使用Vue.delete
,新增我们会使用Vue.set
算是Object.defineProperty()
的一个弱点,我们要记一下。