响应式:简而言之,就是当数据发生改变的时候,视图会重新渲染,更新为最新的值。
初识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()的一个弱点,我们要记一下。