如何追踪变化?
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter
。
数据代理的另一个说法是数据劫持,当我们在访问或者修改对象的某个属性时,数据劫持可以拦截这个行为并进行额外的操作或者修改返回的结果。我们知道Vue响应式系统的核心就是数据代理,代理使得数据在访问时进行依赖收集,在修改更新时对依赖进行更新,这是响应式系统的核心思路。
Object.defineProperty
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
基本用法:Object.defineProperty(obj, prop, descriptor)
Object.defineProperty()
可以用来精确添加或修改对象的属性,只需要在descriptor对象中将属性特性描述清楚,descriptor的属性描述符有两种形式,一种是数据描述符,另一种是存取描述符,我们分别看看各自的特点。
数据描述符,它拥有四个属性配置
configurable
:数据是否可删除,可配置enumerable
:属性是否可枚举value
:属性值,默认为undefinedwritable
:属性是否可读写
存取描述符,它同样拥有四个属性选项configurable
:数据是否可删除,可配置enumerable
:属性是否可枚举get
:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。set
:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。- 需要注意的是: 数据描述符的
value
,writable
和 存取描述符中的get
,set
属性不能同时存在,否则会抛出异常。*然而1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33function observe(data){
if(!data || typeof data !== 'object'){
return
}
Object.keys(data).forEach(function(key){
defineReactive(data,key,data[key])
})
}
function defineReactive(data,key,val){
Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get(){
console.log('获取值')
return val
},
set(newVal){
if(val === newVal) return
console.log('监听到值变化了',val,'-->',newVal)
val = newVal
}
})
}
var arr = [1,2,3]
var obj = {name:'lisi'}
observe(arr)
observe(obj)
obj.age = 18 //拦截不到
obj.name //获取值
obj.name = 'programmer' //监听到值变化了 lisi -->programmer
arr[1] //获取值
arr[2] = 8 //监听到值变化了 3 -->8
arr[4] = 5 //拦截不到Object.defineProperty
是有缺陷的,比如添加属性是监听不到对象的添加和删除或者数组的变化是无法拦截的。
Proxy
为了解决像数组这类无法进行数据拦截,以及深层次的嵌套问题,es6引入了Proxy
的概念,它是真正在语言层面对数据拦截的定义。和Object.defineProperty
一样,Proxy
可以修改某些操作的默认行为,但是不同的是,Proxy
针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上,。 本质的区别是后者会创建一个新的对象对原对象做代理,外界对原对象的访问,都必须先通过这层代理进行拦截处理。而拦截的结果是我们只要通过操作新的实例对象就能间接的操作真正的目标对象了。针对Proxy,下面是基础的写法:
语法:const p = new Proxy(target, handler)
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。1
2
3
4
5
6
7
8
9
10
11
12
13var obj = {b:18}
var nobj = new Proxy(obj, {
get(target, key, receiver) {
console.log('获取值')
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('设置值')
return Reflect.set(target, key, value, receiver)
}
})
nobj.a = '代理'
console.log(obj) //设置值 {b:18,a: "代理"}Proxy
能监听数组的变化,添加
,删除
,修改
等。
依赖收集
为什么要收集依赖?
举个例子:
1 | <template> |
该模板中使用了数据name,所以它发生变化时,要向使用了它的地方发送通知。总结一句话就是在getter中收集依赖,在setter中触发依赖
订阅者 Dep
我们把依赖收集的代码封装成一个Dep类,它帮助我们管理依赖。
1 | class Dep { |
用 addSub
方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
用 notify
方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。
观察者 Watcher
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
1 | class Watcher { |
以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。
收集依赖
1 | function observe (obj) { |
相关流程如下图
在 new Vue() 后, Vue 会调用_init
函数进行初始化,也就是init 过程,在 这个过程Data通过Observer
转换成了getter/setter
的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
当render function 执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。
在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。