前言:
Vue3 已经出来很久了,相信大家很多都已经在使用Vue3去生产了,但是Vue3 究竟比 Vue2 好在哪里呢?今天一起深入学习一下 Vue3的响应式原理,顺便说一说 Vue3的响应式到底比Vue2的响应式好在哪里,篇幅有点长大家一起细细品味!
回顾 Vue2 的响应式原理
1. 基本使用
语法:Object.defineProerty(obj, prop, descriptor)
作用:在一个对象定义一个新属性,或者修改一个对象的现有属性,并返回一个对象
参数:
要添加属性的对象
要定义或修改的属性的名称或 [Symbol]
要定义或修改的属性描述符
看一个简单的例子
<script type="text/javaScript"> let person = {} let personName = 'Barry' // 在 person 对象上添加属性 nameB,值为personName Object.defineProperty(person,'nameB',{ //但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true //默认不可以修改,可:wirtable:true //默认不可以删除,可:configurable:true get(){ console.log('tigger get'); return personName }, set(val){ console.log('trigger set'); personName = val }, }) //当读取person对象的nameB属性时,触发get方法 console.log(person.nameB) //当修改personName时,重新访问person.nameB发现修改成功 personName = 'liming' console.log(person.nameB) // 对person.namep进行修改,触发set方法 person.nameB = 'huahua' console.log(person.nameB) </script>
通过这种方式,我们成功监听了person上的name属性的变化。
2.监听对象上的多个属性
上面的使用中,我们只监听了一个属性的变化,但是在实际情况中,我们通常需要一次监听多个属性的变化。
这时我们需要配合Object.keys(obj)进行遍历。这个方法可以返回obj对象身上的所有可枚举属性组成的字符数组。(其实用for in遍历也可以)
let person = { name:'Barry', age:22 } console.log(Object.keys(person)); // ['name', 'age']
根据上面的 API ,我们就可以遍历劫持对象上的所有属性,但是结果我们发现并达不到效果,下面是写的一个错误版本:
let person = { name:'Barry', age:22 } console.log(Object.keys(person)); /** * Object.defineProperty 复杂使用 */ Object.keys(person).forEach(key=>{ Object.defineProperty(person,key,{ get(){ return person[key] }, set(val){ console.log(`modify person object ${key}`); person[key] = val } }) }) console.log(person.age);
看起来写的并没有问题,但是试着运行一下,你会发现和我报一样的错误---栈溢出
这是为什么呢?
让我们聚焦在 get 方法里面,我们在访问 person 身上的属性时,就会触发 get 方法,返回 person[key] ,但是访问 person[key] 也会触发 get 方法,导致递归调用,最终栈溢出。
这也引出了我们下面的方法,我们需要设置一个中转 Observer,来让 get 中 return 的值并不是直接访问 obj[key]
let person = { name:'Barry', age:22 } console.log(Object.keys(person)); /** * Object.defineProperty 复杂使用(正确版本) */ // 实现一个响应式函数 function defineProperty(obj,key,val) { Object.defineProperty(obj,key,{ get(){ console.log(`trigger ${key} property`); return val }, set(newVal){ console.log(`${key} set property ${newVal}`); val = newVal } }) } // 实现一个遍历函数 Observer function observer(obj){ Object.keys(obj).forEach(key=>{ defineProperty(obj,key,obj[key]) }) } observer(person); console.log(person.name); person.age = 30; console.log(person.age);
3.深度监听一个对象
那么我们如何解决对象中嵌套一个对象的情况呢?其实可以在上述代码中加一个递归,然后利用递归来轻松实现。
我们可以观察到,其实 Observer 就是我们想要实现的监听函数,我们预期的目标是 : 只要把对象传入其中,就可以实现对这个对象的属性监视,即使该对象的属性也是一个对象
具体代码如下:
function defineProperty(obj,key,val) { if(typeof val === 'object'){ observer(val) } Object.defineProperty(obj,key,{ get(){ console.log(`trigger ${key} property`); return val }, set(newVal){ console.log(`${key} set property ${newVal}`); val = newVal } }) }
当然啦,我们也要在observer里面加一个递归停止的条件:
function observer(obj){ if(typeof obj !== 'object' || obj === null){ return } Object.keys(obj).forEach(key=>{ defineProperty(obj,key,obj[key]) }) }
其实到这里就差不多解决了,但是还有一个小问题,如果对某属性进行修改时,如果原本的属性值是一个字符串,但是我们重新赋值了一个对象,我们要如何监听新添加的对象的所有属性呢?其实也很简单,只需要修改set函数:
set(newVal){ console.log(`${key} set property ${newVal}`); if(typeof val === 'object'){ observer(key) } val = newVal }
4.监听数组
那么如果对象的属性是一个数组呢?我们要如何实现监听?
请看下面一段代码:
let hobby = ['抽烟','喝酒','烫头'] let person = { name:'Barry', age:22 } // 把 hobby 作为 person 属性监听 Object.defineProperty(person,'hobby',{ get(){ console.log('tigger get'); return hobby }, set(newVal){ console.log('tigger set',newVal); hobby = newVal } }) console.log(person.hobby); person.hobby = ['看书','游泳','听歌'] person.hobby.push('游泳')
我们发现,通过 push方法给数组增加的元素,set方法是监听不到的。
事实上,通过索引访问或者修改数组中已经存在的元素,是可以触发get和set的,但是对于通过push、unshift增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到。另外, 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。
在Vue2.x中,通过重写Array原型上的方法解决了这个问题,此处就不展开说了,有兴趣的uu可以再去了解下~
vue能监听到数组变化的方法有哪些?为什么这些方法能监听到呢?
https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B
Vue 源码