响应式原理
阅读之前需了解的知识
- MVVM模式
- 观察者模式和发布者-订阅者模式
- Object.defineProperty与ES6中的Proxy
- 数据劫持
- 原型对象和原型链
变化侦测
变化侦测就是侦测数据的变化。从Vue2.0开始,引入了虚拟DOM,将更新粒度调整为中等程度,也就是一个状态所绑定的依赖不再是具体的dom节点,而是一个组件。当状态变化之后,会通知到组件,组件内部再使用虚拟dom进行比对。这样可以大大降低依赖的数量,从而降低依赖追踪所消耗的内存。
图:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
下面将具体介绍vue中是如何侦测数据的变化的,从而实现数据驱动视图变化。
核心实现类
-
Observer
利用 Object.defineProperty 给对象的属性添加getter和setter,用于依赖收集和派发更新。使数据的变化可以被观察到!
-
Dep
收集当前响应式对象的依赖关系,每个响应式对象包括其子对象都拥有一个Dep实例(Dep.subs数组是watcher实例数组),当数据发生变更的时候,通过dep.notify()通知数组里面的所有watcher让其触发更新。
-
Watcher
观察者对象,就是依赖。实例分为render(渲染) watcher、computed(计算) watcher、user(侦测器) watcher。
依赖中记录了所有数据属性以及一些对响应式数据的操作的包装,可以响应数据的变化。
依赖收集
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集。
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集。
- render()的过程,触发 render watcher 依赖收集。
- re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcher 的订阅,重新对dep.subs赋值,进行新一轮依赖的收集。
派发更新
- 组件中响应的数据发生变化,触发 setter 的逻辑。
- 调用 dep.notify()通知 subs数组中所有的Watcher 实例进行更新操作。
- 每一个 watcher 调用 update 方法触发视图更新或用户的某个回调函数。
实现原理
- Data通过Observer转换成了getter/setter的形式(响应式数据)来追踪变化。
当外界通过watcher读取数据的时候,会触发getter从而将watcher添加到依赖中。
数据发生变化后,会触发setter,从而向Dep中的依赖(wathcer)发送通知。
wathcer接收到通知之后,会向外界发送通知,之后可能会触发视图更新,也可能会触发用户的某个回调函数等。
核心代码
/*
* 数组变化侦测 array.js
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
/**
* Observe a list of Array items.
* 数组中新操作的对象进行响应式处理
*/
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
总结
vue.js采用数据劫持结合发布-订阅模式,通过Object.defineproperty来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。
注意事项——当前实现存在的不足
由于js的限制,vue不能检测数组和对象的变化。
- 对象
Vue 无法检测 property 的添加或移除。
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
解决方法:
使用 Vue.set(object, propertyName, value) 方法 / vm.$set向嵌套对象添加响应式 property。
Vue.set(vm.someObject, 'b', 2)
this.$set(this.someObject,'b',2)
- 数组
Vue 不能检测以下数组的变动:
1、当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
2、当你修改数组的长度时,例如:vm.items.length = newLength
解决方法:
用Vue.set / vm.$set / Array.prototype.splice 实现和 vm.items[indexOfItem] = newValue
相同的效果。
Vue.set(vm.items, indexOfItem, newValue)
vm.$set(vm.items, indexOfItem, newValue)
vm.items.splice(indexOfItem, 1, newValue)
用 Array.prototype.splice 实现和 vm.items.length = newLength
相同的效果。
vm.items.splice(newLength)
思考
Watcher 和 Dep 的关系
watcher中(响应式数据被读取时第一次触发getter时当前watcher也会记录自己被收集进当前的dep)实例化了dep,并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher进行更新。
vue中是如何检测数组变化的?
函数劫持。
Vue通过原型拦截的方式重写了数据的7个方法,首先获取数组的Observer对象,如果有加入新的值,就调用observeArray对新的值进行响应式处理,然后手动调用notify,派发更新,渲染页面。
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
Vue为什么不把所有的数据都放到data?
data用来存放绑定的数据。data中的数据都会增加getter、setter,会收集对应的watcher。
如果data里的数据是属于纯展示的数据,根本不需要对这个数据进行监听,特别是一些复杂的列表/对象,放进data中会浪费性能。
可以选择放进computed,因为如果computed是直接返回一个没有引用其他实例属性的值,即没有任何访问响应式数据(如data/props上的属性/其他依赖过的computed等)的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项。
计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算,所以使用computed会更加节约内存。
Vue 为什么不允许动态添加根级响应式 property?
一方面消除了依赖项追踪系统中的一类边界情况,也使Vue实例能更好地配合类型检查系统工作。因为在data对象上才能让Vue将它转换为响应式的数据。
另一方面是在data中提前声明所有的响应式property,会使组件状态的结构更加清晰,便于维护。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!