如何追踪变化?

当你把一个普通的 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:属性值,默认为undefined
  • writable:属性是否可读写
    存取描述符,它同样拥有四个属性选项
  • configurable:数据是否可删除,可配置
  • enumerable:属性是否可枚举
  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。
  • 需要注意的是: 数据描述符的valuewritable 和 存取描述符中的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
    33
    function 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
    13
    var 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
2
3
<template>
<h1>{{name}}</h1>
</template>

该模板中使用了数据name,所以它发生变化时,要向使用了它的地方发送通知。总结一句话就是在getter中收集依赖,在setter中触发依赖

订阅者 Dep

我们把依赖收集的代码封装成一个Dep类,它帮助我们管理依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}

addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者 Watcher

当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}

以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

收集依赖

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
33
34
35
36
37
38
39
40
41
42
43
44
45
function observe (obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
observe(value) // 递归子属性
let dp = new Dep() //新增
Object.defineProperty(obj, key, {
enumerable: true, //可枚举(可以遍历)
configurable: true, //可配置(比如可以删除)
get: function reactiveGetter () {
console.log('get', value) // 监听
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
},
set: function reactiveSetter (newVal) {
observe(newVal) //如果赋值是一个对象,也要递归子属性
if (newVal !== value) {
console.log('set', newVal) // 监听
render()
value = newVal
// 执行 watcher 的 update 方法
dp.notify() //通知更新
}
}
})
}
}

class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
console.log('模拟视图渲染');
}
}

相关流程如下图

在 new Vue() 后, Vue 会调用_init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。
当render function 执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。
在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。