从源码分析Vue2和Vue3的响应式原理
前言
Vue2和Vue3的响应式原理一直是前端面试中的高频考点,如果你还只知道Vue2通过defineProperty
方式实现,Vue3通过代理的方式实现,是不是就太浅显了。那本文带大家从源码去解读他们的实现,响应式实现主要分为三步:数据劫持、收集依赖、派发更新。
Vue2响应式原理
本小节涉及的完整代码github源码链接,这是简化过的源码,添加了注释方便阅读。
整个过程就像上面这张图一样,浏览器会触发’Touch’,这是浏览器在编译文件的过程中完成对所有的HTML中的{{}}
、v-text、v-model等涉及响应式的依赖,对每个依赖new Watcher
,作为后面的订阅者,因为响应式的目的就是自动完成更新这些订阅者。
- 数据劫持:在数据劫持阶段将data中的数据添加响应式(对象会以递归的形式去添加)
- 收集依赖:针对data中每个变量
new Dep
,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新 - 派发更新:watcher对象在创建过程会传入updata用到的cb方法,该方法会去更改Dom上
在new Vue
的过程中其实就已经完成了数据劫持和依赖收集,
/* vue.js */ |
数据劫持
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
完成数据劫持: 把这些 property 全部转为 getter/setter。并且在getter时调用dep.js收集依赖,在setter中调用dep.js的notify方法更新所有依赖的watcher。
注意:对象会以递归的形式去添加响应式
/** |
收集依赖
data中每个响应式属性都会new Dep
,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新
/** |
派发更新
组件在解析 {{}}、v-text、v-model
等和依赖相关的内容,每个需要响应式的位置都会创建watcher 实例。派发更新:数据更新过后首先触发setter,接着触发dep的notify
方法,最后触发watcher的update
方法。
Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。在下一个的事件循环“tick”中,Vue 在内部对异步队列尝试使用原生的
Promise.then
、setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
如果要立即访问更新后的数据,直接访问显示是未更新的数据,因为异步任务挂着呢,没执行到Vue.nextTick(callback)
,这样回调函数将在 DOM 更新完成后被调用
// 数据更新后 收到通知之后 调用 update 进行更新 |
问题
对象增删属性检测不到
只能检查到data函数中声明的对象中的所有property,无法检测到添加或移除property;
解决办法:
- 实例前在data中声明,为null也行。
- 使用this.$delete或Vue.delete进行删除属性。
数组检测问题
Vue2响应式不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
解决办法:1.使用this.$set()或Vue.set();2.使用splice
为什么对象能检测到属性变化,而数组检测不到
本质上Object.defineProperty 也能监听数组变化,但是Vue没采用这个去检测数组,因为要监听数组中的每个元素性能开销大,且使用场景太少。
为什么数组的push和pop等方法会产生响应式?
本来数组的一些方法比如push,pop是不会触发getter/setter的。不会触发的原因是因为这是Array原型上的方法,并没有在Array本身上面。但是vue重写了数组原型上的7个方法,就有了响应式。重写的过程使用拦截器实现,就是和 Array.prototype
一样的对象。
Vue2响应式设计模式的体现
观察者模式:dep.js就是观察者模式,监听到改变就notify所有的观察者
发布订阅模式:dep.js扮演消息中心的角色,observer.js扮演观察者(发布者),watcher.js扮演订阅者
Vue3响应式原理
本小节涉及的完整代码github源码链接,这是简化过的源码,添加了注释方便阅读。
vue3没有vue2那些问题,对象中增删改都可以检测到
Vue3的响应式实现可分为两种:
ref:以ref为代表的基础数据类型的响应式,使用 get/set 存取器
实现
reactive:以reactive为代表的引用数据类型的响应式,使用Proxy
配合Reflect
实现的响应式
其实还以分为更多比如toRef
、toRefs
、shallowReactive
、shallowRef
本文就不展开讨论了
下面主要讲reactive
响应式的实现,ref
响应式的实现见第四小节 get/set 存取器
相关内容
这张图用于辅助理解下面的数据劫持、收集依赖、派发更新理解。
数据劫持
使用Proxy
代理的方式实现数据劫持,与Vue2中一样,属性存在引用数据类型会触发递归,在getter中调用track
方法收集依赖,trigger
方法派发更新。
// 判断是否为对象 ,注意 null 也是对象 |
收集依赖
结合下图不难理解,在track中完成依赖的收集的过程是,先找targetMap
,再找depsMap
,最后找actieEffect
,没有则创建Map或者添加effect。
- targetMap:key为响应式对象的引用,value为depsMap
- depsMap:key响应式对象的属性,value为set类型表示该属性的所有依赖
- actieEffect:表示触发更新的回调就像Vue2的dp函数
// activeEffect 表示当前正在走的 effect |
派发更新
派发更新就是去调用触发更新的所有依赖。通过target找到是哪个对象需要更新,再通过key找到是哪个属性需要更新,最后调用该属性的所有依赖的effect
更新。
// trigger 响应式触发 |
问题
直通过索引设置可能隐式导致length问题
当通过索引设置响应式数组的时候,有可能会隐式修改数组的 length 属性,例如设置的索引值大于数组当前的长度时,那么就要更新数组的 length 属性,因此在触发当前的修改属性的响应之外,也需要触发与 length 属性相关依赖进行重新执行。
所以我们要尽量避免这个问题:
- 不要去直接修改响应式数组的length属性
- 通过数组索引修改响应式数组时,不要将数组索引大于数组的length属性
ref和reactive的响应式区别
- ref:把一个基础类型包装成一个有value响应式对象(使用
get/set 存取器
,来进行追踪和触发),如果是普通对象就调用 reactive 来创建响应式对象。 - reactive:返回proxy对象,这个reactive可以深层次递归,如果发现子元素存在引用类型,递归reactive处理
Object.defineProperty
和 get/set 存取器
的区别
Object.defineProperty
是一个较低级别的操作,它只能用于单个属性,并且需要显式地定义每个属性的描述符。这在大量属性定义时可能会显得冗长和繁琐。因此,在 ES6 之后,通常更推荐使用get/set 存取器
来创建访问器属性
Object.defineProperty
实现响应式
const obj = {}; |
get/set 存取器
实现的响应式
const obj = { |
为什么要使用Reflect(反射)
Reflect是ES6出现的新特性,代码运行期间用来设置或获取对象成员,代替原始的操作,更加安全、语义化;Object.getPrototypeOf
=> Reflect.getPrototypeOf
target[propName]
=> Reflect.get(target,propName)
target[propName] = value
=> Reflect.set(target,propName,value)
返回true和falsedelete target[propName]
=> Reflect.deleteProperty(target,propName)
返回true和false 表示执行成功还是失败
总结
Vue2的响应式实现可分为三步:
- 数据劫持:在数据劫持阶段将data中的数据添加响应式(对象会以递归的形式去添加)
- 收集依赖:针对data中每个变量
new Dep
,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新 - 派发更新:watcher对象在创建过程会传入updata用到的cb方法,该方法会去更改Dom上
Vue2响应式还存在问题:对象增删属性检、数组使用index修改和直接修改length不会触发响应式
Vue3的响应式实现分为两种:
- 基础数据类型的响应式,使用
get/set 存取器
实现 - 引用数据类型的响应式,使用
Proxy
配合Reflect
实现的响应式,实现过程中也可分为数据劫持、收集依赖、派发更新三步去实现
Vue3响应式还存在问题:直接修改length、将数组索引大于数组的length不会触发响应式
感谢小伙伴们的耐心观看,本文为笔者个人学习记录,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!